uploadcare-ruby 1.2.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -1
  3. data/.rspec +1 -0
  4. data/.travis.yml +19 -5
  5. data/CHANGELOG.md +12 -30
  6. data/README.md +149 -72
  7. data/Rakefile +1 -1
  8. data/UPGRADE_NOTES.md +36 -0
  9. data/lib/uploadcare.rb +16 -22
  10. data/lib/uploadcare/api.rb +3 -1
  11. data/lib/uploadcare/api/file_list_api.rb +15 -4
  12. data/lib/uploadcare/api/file_storage_api.rb +34 -0
  13. data/lib/uploadcare/api/group_list_api.rb +13 -4
  14. data/lib/uploadcare/api/raw_api.rb +10 -16
  15. data/lib/uploadcare/api/uploading_api.rb +45 -84
  16. data/lib/uploadcare/api/uploading_api/upload_params.rb +72 -0
  17. data/lib/uploadcare/api/validators/file_list_options_validator.rb +73 -0
  18. data/lib/uploadcare/api/validators/group_list_options_validator.rb +49 -0
  19. data/lib/uploadcare/resources/file_list.rb +6 -33
  20. data/lib/uploadcare/resources/group_list.rb +6 -23
  21. data/lib/uploadcare/resources/resource_list.rb +83 -0
  22. data/lib/uploadcare/rest/connections/api_connection.rb +25 -3
  23. data/lib/uploadcare/rest/connections/upload_connection.rb +3 -2
  24. data/lib/uploadcare/version.rb +1 -1
  25. data/spec/api/file_list_api_spec.rb +95 -0
  26. data/spec/api/file_storage_api_spec.rb +88 -0
  27. data/spec/api/group_list_api_spec.rb +59 -0
  28. data/spec/api/raw_api_spec.rb +12 -12
  29. data/spec/api/uploading_api/upload_params_spec.rb +99 -0
  30. data/spec/api/uploading_api_spec.rb +59 -0
  31. data/spec/resources/file_list_spec.rb +13 -52
  32. data/spec/resources/file_spec.rb +6 -3
  33. data/spec/resources/group_list_spec.rb +15 -20
  34. data/spec/rest/api_connection_spec.rb +1 -1
  35. data/spec/shared/resource_list.rb +188 -0
  36. data/spec/spec_helper.rb +11 -1
  37. data/spec/uploadcare_spec.rb +9 -32
  38. data/spec/utils/parser_spec.rb +34 -36
  39. data/uploadcare-ruby.gemspec +7 -6
  40. metadata +52 -37
  41. data/lib/uploadcare/utils/user_agent.rb +0 -44
  42. data/spec/uploading/uploading_multiple_spec.rb +0 -43
  43. data/spec/uploading/uploading_spec.rb +0 -40
  44. data/spec/utils/user_agent_spec.rb +0 -46
@@ -0,0 +1,49 @@
1
+ module Uploadcare
2
+ module Validators
3
+
4
+ class GroupListOptionsValidator
5
+ SUPPORTED_KEYS = [:from, :ordering, :limit]
6
+
7
+ def initialize(options)
8
+ @options = options
9
+ end
10
+
11
+ def validate
12
+ check_for_unsupported_keys(@options)
13
+
14
+ validate_limit(@options[:limit])
15
+ validate_ordering(@options[:ordering])
16
+ validate_from(@options[:from])
17
+ end
18
+
19
+ private
20
+
21
+ def check_for_unsupported_keys(options)
22
+ unsupported_keys = options.keys.reject{|k,_| SUPPORTED_KEYS.include?(k)}
23
+ error("Unknown options: #{unsupported_keys}") if unsupported_keys.any?
24
+ end
25
+
26
+ def validate_ordering(ordering)
27
+ return if !ordering || ordering =~ /^-?datetime_created$/
28
+ error("Unknown value for :ordering option: #{ordering.inspect}")
29
+ end
30
+
31
+ def validate_from(from)
32
+ return if from.nil? || from.to_s =~ /^\d{4}-\d{2}-\d{2}T\d{2}.*/
33
+ error(":from value should be a DateTime or an iso8601 string, "\
34
+ "#{from.inspect} given")
35
+ end
36
+
37
+ def validate_limit(limit)
38
+ return if limit.nil? || (limit.is_a?(Integer) && (1..1000).include?(limit))
39
+ error(":limit should be a positive integer from 1 to 1000, "\
40
+ "#{limit.inspect} given")
41
+ end
42
+
43
+ def error(message)
44
+ raise ArgumentError, message
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -1,41 +1,14 @@
1
1
  require 'ostruct'
2
+ require_relative 'resource_list'
2
3
 
3
4
  module Uploadcare
4
5
  class Api
5
- class FileList < OpenStruct
6
- def initialize api, data
7
- @api = api
6
+ class FileList < ResourceList
7
+ private
8
8
 
9
- unless data["results"].nil?
10
- data["results"].map! do |file|
11
- Uploadcare::Api::File.new @api, file["uuid"], file
12
- end
13
- end
14
-
15
- super data
16
- end
17
-
18
- # Array-like behavior
19
- def [] index
20
- results[index] if defined?(:results)
21
- end
22
-
23
- def to_a
24
- results if defined?(:results)
25
- end
26
-
27
- # List navigation
28
- def next_page
29
- @api.file_list(page+1) unless send(:next).nil?
30
- end
31
-
32
- def go_to index
33
- @api.file_list(index) unless index > pages
34
- end
35
-
36
- def previous_page
37
- @api.file_list(page-1) unless previous.nil?
9
+ def to_resource(api, file_data)
10
+ Uploadcare::Api::File.new(api, file_data['uuid'], file_data)
38
11
  end
39
12
  end
40
13
  end
41
- end
14
+ end
@@ -1,31 +1,14 @@
1
1
  require 'ostruct'
2
+ require_relative 'resource_list'
2
3
 
3
4
  module Uploadcare
4
5
  class Api
5
- class GroupList < OpenStruct
6
- def initialize api, data
7
- @api = api
6
+ class GroupList < ResourceList
7
+ private
8
8
 
9
- unless data["results"].nil?
10
- data["results"].map! do |group|
11
- Uploadcare::Api::Group.new @api, group["id"], group
12
- end
13
- end
14
-
15
- super data
16
- end
17
-
18
- def [] index
19
- results[index] if defined?(:results)
20
- end
21
-
22
- def to_a
23
- results if defined?(:results)
24
- end
25
-
26
- def groups
27
- results if defined?(:results)
9
+ def to_resource(api, group_data)
10
+ Uploadcare::Api::Group.new api, group_data["id"], group_data
28
11
  end
29
12
  end
30
13
  end
31
- end
14
+ end
@@ -0,0 +1,83 @@
1
+ require 'ostruct'
2
+
3
+ module Uploadcare
4
+ class Api
5
+ class ResourceList
6
+ include Enumerable
7
+
8
+ extend Forwardable
9
+ def_delegator :@data, :meta
10
+ def_delegator :@data, :objects
11
+
12
+ attr_reader :options
13
+
14
+ def initialize(api, data, options)
15
+ @api = api
16
+ @data = build_data(data)
17
+ @options = options.dup.freeze
18
+ end
19
+
20
+ def [](index)
21
+ first(index + 1).last
22
+ end
23
+
24
+ def each
25
+ return enum_for(:each) unless block_given?
26
+
27
+ resource_enumerator.each { |object| yield object }
28
+
29
+ self
30
+ end
31
+
32
+ def total
33
+ meta['total']
34
+ end
35
+
36
+ def loaded
37
+ objects.size
38
+ end
39
+
40
+ def fully_loaded?
41
+ meta['next'].nil?
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :api
47
+
48
+ def build_data(data_hash)
49
+ OpenStruct.new(
50
+ meta: data_hash.reject{|k, _| k == 'results'}.freeze,
51
+ objects: data_hash['results'].map{|object| to_resource(api, object)}
52
+ )
53
+ end
54
+
55
+ def get_next_page
56
+ return nil if fully_loaded?
57
+
58
+ next_page = build_data(api.get(@data.meta['next']))
59
+
60
+ @data = OpenStruct.new(
61
+ meta: next_page.meta,
62
+ objects: objects + next_page.objects
63
+ )
64
+
65
+ next_page
66
+ end
67
+
68
+ def to_resource(*args)
69
+ raise NotImplementedError, 'You must define this method in a child class'
70
+ end
71
+
72
+ def resource_enumerator
73
+ Enumerator.new do |yielder|
74
+ objects.each { |obj| yielder << obj }
75
+
76
+ while next_page = get_next_page do
77
+ next_page.objects.each { |obj| yielder << obj }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -10,14 +10,14 @@ module Uploadcare
10
10
  auth_strategy = Auth.strategy(options)
11
11
 
12
12
  frd.headers['Accept'] = "application/vnd.uploadcare-v#{options[:api_version]}+json"
13
- frd.headers['User-Agent'] = UserAgent.new.call(options)
13
+ frd.headers['User-Agent'] = Uploadcare::user_agent(options)
14
14
 
15
15
  # order of middleware matters!
16
16
 
17
- # url_encoded changes request body and thus should be before
17
+ # :json middleware changes request body and thus should be before
18
18
  # uploadcare_auth which uses it to sign requests when secure auth
19
19
  # strategy is being used
20
- frd.request :url_encoded
20
+ frd.request :json
21
21
  frd.request :uploadcare_auth, auth_strategy
22
22
 
23
23
  frd.response :uploadcare_raise_error
@@ -27,6 +27,28 @@ module Uploadcare
27
27
  frd.adapter :net_http # actually, default adapter, just to be clear
28
28
  end
29
29
  end
30
+
31
+ # NOTE: Faraday doesn't support body in DELETE requests, but
32
+ # Uploadcare API v0.5 requires clients to send array of UUIDs with
33
+ # `DELETE /files/storage/` requests.
34
+ #
35
+ # This is on override of the original Faraday::Connection#delete method.
36
+ #
37
+ # As for now, there are no DELETE requests in Uploadcare REST API
38
+ # which require params to be sent as URI params, so for simplicity
39
+ # this method send all params in a body.
40
+ def delete(url = nil, params = nil, headers = nil)
41
+ run_request(:delete, url, nil, headers) { |request|
42
+ # Original line from Faraday::Connection#delete method
43
+ # request.params.update(params) if params
44
+
45
+ # Monkey patch
46
+ request.body = params if params
47
+
48
+ yield(request) if block_given?
49
+ }
50
+ end
51
+
30
52
  end
31
53
  end
32
54
  end
@@ -9,11 +9,12 @@ module Uploadcare
9
9
  super ssl: { ca_path: ca_path }, url: options[:upload_url_base] do |frd|
10
10
  frd.request :multipart
11
11
  frd.request :url_encoded
12
- frd.adapter :net_http
13
- frd.headers['User-Agent'] = UserAgent.new.call(options)
12
+ frd.headers['User-Agent'] = Uploadcare::user_agent(options)
14
13
 
15
14
  frd.response :uploadcare_raise_error
16
15
  frd.response :uploadcare_parse_json
16
+
17
+ frd.adapter :net_http
17
18
  end
18
19
  end
19
20
  end
@@ -1,3 +1,3 @@
1
1
  module Uploadcare
2
- VERSION = "1.2.2"
2
+ VERSION = '2.0.0'.freeze
3
3
  end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+
3
+ describe Uploadcare::FileListApi do
4
+ let(:api){ API }
5
+ subject{ api.file_list(limit: 1) }
6
+
7
+ before(:each){ allow(api).to receive(:get){ {'results' => []} } }
8
+
9
+ it 'returns a file list' do
10
+ expect( subject ).to be_a(Uploadcare::Api::FileList)
11
+ end
12
+
13
+ it 'stores options in a file list object' do
14
+ expect( subject.options ).to eq({limit: 1})
15
+ end
16
+
17
+ describe 'validation' do
18
+ it 'passes validation when no options given' do
19
+ expect{ api.file_list }.not_to raise_error
20
+ end
21
+
22
+ it "validates that options don't have unsupported keys" do
23
+ expect{ api.file_list(unknown: 1) }.to raise_error(ArgumentError)
24
+ end
25
+
26
+ it 'validates that :limit is an integer from 1 to 1000' do
27
+ expect{ api.file_list(limit: 1) }.not_to raise_error
28
+ expect{ api.file_list(limit: 395) }.not_to raise_error
29
+ expect{ api.file_list(limit: 1000) }.not_to raise_error
30
+
31
+ expect{ api.file_list(limit: 1.0) }.to raise_error(ArgumentError)
32
+ expect{ api.file_list(limit: -1) }.to raise_error(ArgumentError)
33
+ expect{ api.file_list(limit: 0) }.to raise_error(ArgumentError)
34
+ expect{ api.file_list(limit: 1001) }.to raise_error(ArgumentError)
35
+ expect{ api.file_list(limit: false) }.to raise_error(ArgumentError)
36
+ end
37
+
38
+ it 'validates that :stored is a boolean' do
39
+ expect{ api.file_list(stored: true) }.not_to raise_error
40
+ expect{ api.file_list(stored: false) }.not_to raise_error
41
+
42
+ expect{ api.file_list(stored: 'yes') }.to raise_error(ArgumentError)
43
+ end
44
+
45
+ it 'validates that :removed is a boolean' do
46
+ expect{ api.file_list(removed: true) }.not_to raise_error
47
+ expect{ api.file_list(removed: false) }.not_to raise_error
48
+
49
+ expect{ api.file_list(removed: 'yes') }.to raise_error(ArgumentError)
50
+ end
51
+
52
+ valid_ordering = %w{size -size datetime_uploaded -datetime_uploaded}
53
+ it "validates that :ordering is in [#{valid_ordering.join(', ')}]" do
54
+ valid_ordering.each do |valid_value|
55
+ expect{ api.file_list(ordering: valid_value) }.not_to raise_error
56
+ end
57
+
58
+ expect{ api.file_list(ordering: 'yes') }.to raise_error(ArgumentError)
59
+ end
60
+
61
+ describe 'from' do
62
+ context 'when ordering is "size" or "-size"' do
63
+ let(:opts){ {ordering: ['size', '-size'].sample} }
64
+
65
+ it 'validates that :from is a non-negative integer' do
66
+ valid = [0, 100500]
67
+ valid.each do |value|
68
+ expect{ api.file_list(opts.merge(from: value)) }.not_to raise_error
69
+ end
70
+
71
+ invalid = [-1, 200.0, "string", false]
72
+ invalid.each do |value|
73
+ expect{ api.file_list(opts.merge(from: value)) }.to raise_error(ArgumentError)
74
+ end
75
+ end
76
+ end
77
+
78
+ context 'when ordering is "datetime_uploaded", "-datetime_uploaded" or nil' do
79
+ let(:opts){ {ordering: ['datetime_uploaded', '-datetime_uploaded', nil].sample} }
80
+
81
+ it 'validates that :from.to_s is a iso8601 string' do
82
+ valid = [DateTime.now, DateTime.now.iso8601, "2017-01-01T15"]
83
+ valid.each do |value|
84
+ expect{ api.file_list(opts.merge(from: value)) }.not_to raise_error
85
+ end
86
+
87
+ invalid = [Date.today, Time.now, DateTime.now.rfc2822, "2017-01-01", 123, false]
88
+ invalid.each do |value|
89
+ expect{ api.file_list(opts.merge(from: value)) }.to raise_error(ArgumentError)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,88 @@
1
+ require 'spec_helper'
2
+
3
+ describe Uploadcare::FileStorageApi do
4
+ let(:api){ API }
5
+ let(:file){ api.file_list(limit: 1).first || api.upload(IMAGE_URL) }
6
+
7
+ shared_examples 'batch action on files' do
8
+ let(:uuids) { ["dc2c175d-a3b5-4435-b4f4-fae77bbe5597", "cea319aa-6e17-4172-8722-8dd7c459a523"] }
9
+ let(:files) { uuids.map { |uuid| Uploadcare::Api::File.new(api, uuid) } }
10
+ let(:api_endpoint) { "/files/storage/" }
11
+
12
+ it 'accepts array of uuids' do
13
+ expect(api).to receive(http_method)
14
+ expect { subject.call(uuids) }.not_to raise_error
15
+ end
16
+
17
+ it 'accepts enumerable containing Uploadcare::Api::File objects' do
18
+ expect(api).to receive(http_method)
19
+ expect { subject.call(files) }.not_to raise_error
20
+ end
21
+
22
+ it 'converts Uploadcare::Api::File-s to uuids' do
23
+ expect(api).to receive(http_method).with(api_endpoint, uuids)
24
+ subject.call(files)
25
+ end
26
+
27
+ context 'when input contains something other than UUIDs or Uploadcare::Api::File-s' do
28
+ it 'raises ArgumentError' do
29
+ ['not-an-uuid', nil, 1].each do |wrong_input_value|
30
+ expect { subject.call([wrong_input_value]) }.to raise_error(ArgumentError)
31
+ end
32
+ end
33
+ end
34
+
35
+ it 'raises ArgumentError if input size is grater then max batch size' do
36
+ stub_const("Uploadcare::FileStorageApi::MAX_BATCH_SIZE", 1)
37
+ expect { subject.call(uuids) }.to raise_error(ArgumentError)
38
+ end
39
+ end
40
+
41
+ describe '#store_files' do
42
+ let(:http_method) { :put }
43
+ subject { ->(objects) { api.store_files(objects) } }
44
+
45
+ it_behaves_like 'batch action on files'
46
+
47
+ describe 'integration test' do
48
+ before { file.tap { |f| wait_until_ready(f) }.delete if file.stored? }
49
+ subject(:store_files) { -> { api.store_files([file]) } }
50
+
51
+ it 'stores files with given uuids' do
52
+ is_expected.to change { file.load!.stored? }.from(false).to(true)
53
+ end
54
+
55
+ it 'returns the API response' do
56
+ expect(store_files.call).to include(
57
+ "status" => "ok",
58
+ "problems" => {},
59
+ "result" => [be_a(Hash)]
60
+ )
61
+ end
62
+ end
63
+ end
64
+
65
+ describe '#delete_files' do
66
+ let(:http_method) { :delete }
67
+ subject { ->(objects) { api.delete_files(objects) } }
68
+
69
+ it_behaves_like 'batch action on files'
70
+
71
+ describe 'integration test' do
72
+ before { file.store if file.deleted? }
73
+ subject(:delete_files) { -> { api.delete_files([file]) } }
74
+
75
+ it 'deletes files with given uuids' do
76
+ is_expected.to change { file.load!.deleted? }.from(false).to(true)
77
+ end
78
+
79
+ it 'returns the API response' do
80
+ expect(delete_files.call).to include(
81
+ "status" => "ok",
82
+ "problems" => {},
83
+ "result" => [be_a(Hash)]
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end