restforce 2.5.4 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +56 -0
  3. data/.rubocop.yml +27 -14
  4. data/.rubocop_todo.yml +128 -81
  5. data/CHANGELOG.md +37 -3
  6. data/CONTRIBUTING.md +3 -3
  7. data/Gemfile +4 -2
  8. data/Guardfile +3 -1
  9. data/LICENSE +1 -1
  10. data/README.md +120 -19
  11. data/Rakefile +2 -1
  12. data/lib/restforce.rb +23 -1
  13. data/lib/restforce/abstract_client.rb +3 -0
  14. data/lib/restforce/attachment.rb +3 -0
  15. data/lib/restforce/client.rb +2 -0
  16. data/lib/restforce/collection.rb +3 -1
  17. data/lib/restforce/concerns/api.rb +20 -14
  18. data/lib/restforce/concerns/authentication.rb +2 -0
  19. data/lib/restforce/concerns/base.rb +2 -0
  20. data/lib/restforce/concerns/batch_api.rb +87 -0
  21. data/lib/restforce/concerns/caching.rb +4 -2
  22. data/lib/restforce/concerns/canvas.rb +3 -0
  23. data/lib/restforce/concerns/connection.rb +26 -20
  24. data/lib/restforce/concerns/picklists.rb +9 -6
  25. data/lib/restforce/concerns/streaming.rb +60 -1
  26. data/lib/restforce/concerns/verbs.rb +3 -1
  27. data/lib/restforce/config.rb +4 -1
  28. data/lib/restforce/data/client.rb +2 -0
  29. data/lib/restforce/document.rb +3 -0
  30. data/lib/restforce/mash.rb +2 -0
  31. data/lib/restforce/middleware.rb +2 -0
  32. data/lib/restforce/middleware/authentication.rb +8 -6
  33. data/lib/restforce/middleware/authentication/password.rb +2 -0
  34. data/lib/restforce/middleware/authentication/token.rb +2 -0
  35. data/lib/restforce/middleware/authorization.rb +3 -1
  36. data/lib/restforce/middleware/caching.rb +3 -1
  37. data/lib/restforce/middleware/custom_headers.rb +2 -0
  38. data/lib/restforce/middleware/gzip.rb +5 -3
  39. data/lib/restforce/middleware/instance_url.rb +7 -3
  40. data/lib/restforce/middleware/logger.rb +2 -0
  41. data/lib/restforce/middleware/mashify.rb +2 -0
  42. data/lib/restforce/middleware/multipart.rb +8 -4
  43. data/lib/restforce/middleware/raise_error.rb +26 -8
  44. data/lib/restforce/patches/parts.rb +2 -0
  45. data/lib/restforce/signed_request.rb +3 -0
  46. data/lib/restforce/sobject.rb +3 -0
  47. data/lib/restforce/tooling/client.rb +5 -3
  48. data/lib/restforce/upload_io.rb +2 -0
  49. data/lib/restforce/version.rb +3 -1
  50. data/restforce.gemspec +19 -12
  51. data/spec/fixtures/sobject/sobject_describe_success_response.json +48 -1
  52. data/spec/integration/abstract_client_spec.rb +51 -7
  53. data/spec/integration/data/client_spec.rb +24 -5
  54. data/spec/spec_helper.rb +2 -0
  55. data/spec/support/client_integration.rb +2 -0
  56. data/spec/support/concerns.rb +2 -0
  57. data/spec/support/event_machine.rb +2 -0
  58. data/spec/support/fixture_helpers.rb +4 -2
  59. data/spec/support/matchers.rb +2 -0
  60. data/spec/support/middleware.rb +3 -1
  61. data/spec/support/mock_cache.rb +4 -2
  62. data/spec/unit/abstract_client_spec.rb +2 -0
  63. data/spec/unit/attachment_spec.rb +2 -0
  64. data/spec/unit/collection_spec.rb +5 -3
  65. data/spec/unit/concerns/api_spec.rb +40 -11
  66. data/spec/unit/concerns/authentication_spec.rb +4 -2
  67. data/spec/unit/concerns/base_spec.rb +2 -0
  68. data/spec/unit/concerns/batch_api_spec.rb +107 -0
  69. data/spec/unit/concerns/caching_spec.rb +2 -0
  70. data/spec/unit/concerns/canvas_spec.rb +3 -1
  71. data/spec/unit/concerns/connection_spec.rb +5 -3
  72. data/spec/unit/concerns/streaming_spec.rb +115 -1
  73. data/spec/unit/config_spec.rb +10 -8
  74. data/spec/unit/data/client_spec.rb +2 -0
  75. data/spec/unit/document_spec.rb +2 -0
  76. data/spec/unit/mash_spec.rb +3 -1
  77. data/spec/unit/middleware/authentication/password_spec.rb +2 -0
  78. data/spec/unit/middleware/authentication/token_spec.rb +2 -0
  79. data/spec/unit/middleware/authentication_spec.rb +3 -1
  80. data/spec/unit/middleware/authorization_spec.rb +2 -0
  81. data/spec/unit/middleware/custom_headers_spec.rb +3 -1
  82. data/spec/unit/middleware/gzip_spec.rb +4 -2
  83. data/spec/unit/middleware/instance_url_spec.rb +2 -0
  84. data/spec/unit/middleware/logger_spec.rb +2 -0
  85. data/spec/unit/middleware/mashify_spec.rb +3 -1
  86. data/spec/unit/middleware/raise_error_spec.rb +34 -11
  87. data/spec/unit/signed_request_spec.rb +2 -0
  88. data/spec/unit/sobject_spec.rb +5 -3
  89. data/spec/unit/tooling/client_spec.rb +2 -0
  90. metadata +38 -20
  91. data/.travis.yml +0 -16
  92. data/Gemfile.travis +0 -8
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  module Concerns
3
5
  module Authentication
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  module Concerns
3
5
  module Base
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'restforce/concerns/verbs'
4
+
5
+ module Restforce
6
+ module Concerns
7
+ module BatchAPI
8
+ extend Restforce::Concerns::Verbs
9
+
10
+ define_verbs :post
11
+
12
+ def batch(halt_on_error: false)
13
+ subrequests = Subrequests.new(options)
14
+ yield(subrequests)
15
+ subrequests.requests.each_slice(25).map do |requests|
16
+ properties = {
17
+ batchRequests: requests,
18
+ haltOnError: halt_on_error
19
+ }
20
+ response = api_post('composite/batch', properties.to_json)
21
+ body = response.body
22
+ results = body['results']
23
+ if halt_on_error && body['hasErrors']
24
+ last_error_index = results.rindex { |result| result['statusCode'] != 412 }
25
+ last_error = results[last_error_index]
26
+ raise BatchAPIError, last_error['result'][0]['errorCode']
27
+ end
28
+ results.map(&:compact)
29
+ end.flatten
30
+ end
31
+
32
+ def batch!(&block)
33
+ batch(halt_on_error: true, &block)
34
+ end
35
+
36
+ class Subrequests
37
+ def initialize(options)
38
+ @options = options
39
+ @requests = []
40
+ end
41
+ attr_reader :options, :requests
42
+
43
+ def create(sobject, attrs)
44
+ requests << { method: 'POST', url: batch_api_path(sobject), richInput: attrs }
45
+ end
46
+
47
+ def update(sobject, attrs)
48
+ id = attrs.fetch(attrs.keys.find { |k, v| k.to_s.casecmp?('id') }, nil)
49
+ raise ArgumentError, 'Id field missing from attrs.' unless id
50
+
51
+ attrs_without_id = attrs.reject { |k, v| k.to_s.casecmp?('id') }
52
+ requests << {
53
+ method: 'PATCH',
54
+ url: batch_api_path("#{sobject}/#{id}"),
55
+ richInput: attrs_without_id
56
+ }
57
+ end
58
+
59
+ def destroy(sobject, id)
60
+ requests << { method: 'DELETE', url: batch_api_path("#{sobject}/#{id}") }
61
+ end
62
+
63
+ def upsert(sobject, ext_field, attrs)
64
+ raise ArgumentError, 'External id field missing.' unless ext_field
65
+
66
+ ext_id = attrs.fetch(attrs.keys.find { |k, v|
67
+ k.to_s.casecmp?(ext_field.to_s)
68
+ }, nil)
69
+ raise ArgumentError, 'External id missing from attrs.' unless ext_id
70
+
71
+ attrs_without_ext_id = attrs.reject { |k, v| k.to_s.casecmp?(ext_field) }
72
+ requests << {
73
+ method: 'PATCH',
74
+ url: batch_api_path("#{sobject}/#{ext_field}/#{ext_id}"),
75
+ richInput: attrs_without_ext_id
76
+ }
77
+ end
78
+
79
+ private
80
+
81
+ def batch_api_path(path)
82
+ "v#{options[:api_version]}/sobjects/#{path}"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  module Concerns
3
5
  module Caching
@@ -6,9 +8,9 @@ module Restforce
6
8
  # block - A query/describe/etc.
7
9
  #
8
10
  # Returns the result of the block
9
- def without_caching(&block)
11
+ def without_caching
10
12
  options[:use_cache] = false
11
- block.call
13
+ yield
12
14
  ensure
13
15
  options.delete(:use_cache)
14
16
  end
@@ -1,8 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  module Concerns
3
5
  module Canvas
4
6
  def decode_signed_request(signed_request)
5
7
  raise 'client_secret not set.' unless options[:client_secret]
8
+
6
9
  SignedRequest.decode(signed_request, options[:client_secret])
7
10
  end
8
11
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  module Concerns
3
5
  module Connection
@@ -13,7 +15,7 @@ module Restforce
13
15
  def middleware
14
16
  connection.builder
15
17
  end
16
- alias_method :builder, :middleware
18
+ alias builder middleware
17
19
 
18
20
  private
19
21
 
@@ -22,40 +24,44 @@ module Restforce
22
24
  @connection ||= Faraday.new(options[:instance_url],
23
25
  connection_options) do |builder|
24
26
  # Parses JSON into Hashie::Mash structures.
25
- unless (options[:mashify] == false)
26
- builder.use Restforce::Middleware::Mashify, self, options
27
+ unless options[:mashify] == false
28
+ builder.use Restforce::Middleware::Mashify, self, options
27
29
  end
28
30
 
29
31
  # Handles multipart file uploads for blobs.
30
- builder.use Restforce::Middleware::Multipart
32
+ builder.use Restforce::Middleware::Multipart
31
33
  # Converts the request into JSON.
32
- builder.request :json
34
+ builder.request :json
33
35
  # Handles reauthentication for 403 responses.
34
36
  if authentication_middleware
35
- builder.use authentication_middleware, self, options
37
+ builder.use authentication_middleware, self, options
36
38
  end
37
39
  # Sets the oauth token in the headers.
38
- builder.use Restforce::Middleware::Authorization, self, options
40
+ builder.use Restforce::Middleware::Authorization, self, options
39
41
  # Ensures the instance url is set.
40
- builder.use Restforce::Middleware::InstanceURL, self, options
42
+ builder.use Restforce::Middleware::InstanceURL, self, options
41
43
  # Caches GET requests.
42
- builder.use Restforce::Middleware::Caching, cache, options if cache
44
+ builder.use Restforce::Middleware::Caching, cache, options if cache
43
45
  # Follows 30x redirects.
44
- builder.use FaradayMiddleware::FollowRedirects
46
+ builder.use FaradayMiddleware::FollowRedirects
45
47
  # Raises errors for 40x responses.
46
- builder.use Restforce::Middleware::RaiseError
48
+ builder.use Restforce::Middleware::RaiseError
47
49
  # Parses returned JSON response into a hash.
48
50
  builder.response :json, content_type: /\bjson$/
49
51
  # Compress/Decompress the request/response
50
- builder.use Restforce::Middleware::Gzip, self, options
52
+ unless adapter == :httpclient
53
+ builder.use Restforce::Middleware::Gzip, self, options
54
+ end
51
55
  # Inject custom headers into requests
52
- builder.use Restforce::Middleware::CustomHeaders, self, options
56
+ builder.use Restforce::Middleware::CustomHeaders, self, options
53
57
  # Log request/responses
54
- builder.use Restforce::Middleware::Logger,
55
- Restforce.configuration.logger,
56
- options if Restforce.log?
58
+ if Restforce.log?
59
+ builder.use Restforce::Middleware::Logger,
60
+ Restforce.configuration.logger,
61
+ options
62
+ end
57
63
 
58
- builder.adapter adapter
64
+ builder.adapter adapter
59
65
  end
60
66
  end
61
67
 
@@ -67,10 +73,10 @@ module Restforce
67
73
  def connection_options
68
74
  { request: {
69
75
  timeout: options[:timeout],
70
- open_timeout: options[:timeout] },
76
+ open_timeout: options[:timeout]
77
+ },
71
78
  proxy: options[:proxy_uri],
72
- ssl: options[:ssl]
73
- }
79
+ ssl: options[:ssl] }
74
80
  end
75
81
 
76
82
  # Internal: Returns true if the middlware stack includes the
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  module Concerns
3
5
  module Picklists
@@ -26,14 +28,13 @@ module Restforce
26
28
  PicklistValues.new(describe(sobject)['fields'], field, options)
27
29
  end
28
30
 
29
- private
30
-
31
31
  class PicklistValues < Array
32
32
  def initialize(fields, field, options = {})
33
33
  @fields = fields
34
34
  @field = field
35
35
  @valid_for = options.delete(:valid_for)
36
36
  raise "#{field} is not a dependent picklist" if @valid_for && !dependent?
37
+
37
38
  replace(picklist_values)
38
39
  end
39
40
 
@@ -60,7 +61,9 @@ module Restforce
60
61
 
61
62
  def controlling_picklist
62
63
  @_controlling_picklist ||= controlling_field['picklistValues'].
63
- find { |picklist_entry| picklist_entry['value'] == @valid_for }
64
+ find do |picklist_entry|
65
+ picklist_entry['value'] == @valid_for
66
+ end
64
67
  end
65
68
 
66
69
  def index
@@ -80,9 +83,9 @@ module Restforce
80
83
  # See http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_des
81
84
  # cribesobjects_describesobjectresult.htm
82
85
  def valid?(picklist_entry)
83
- valid_for = picklist_entry['validFor'].ljust(16, 'A').unpack('m').first.
84
- unpack('q*')
85
- (valid_for[index >> 3] & (0x80 >> index % 8)) != 0
86
+ valid_for = picklist_entry['validFor'].ljust(16, 'A').unpack1('m').
87
+ unpack('C*')
88
+ (valid_for[index >> 3] & (0x80 >> index % 8)).positive?
86
89
  end
87
90
  end
88
91
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  module Concerns
3
5
  module Streaming
@@ -7,7 +9,8 @@ module Restforce
7
9
  # block - A block to run when a new message is received.
8
10
  #
9
11
  # Returns a Faye::Subscription
10
- def subscribe(channels, &block)
12
+ def subscribe(channels, options = {}, &block)
13
+ Array(channels).each { |channel| replay_handlers[channel] = options[:replay] }
11
14
  faye.subscribe Array(channels).map { |channel| "/topic/#{channel}" }, &block
12
15
  end
13
16
 
@@ -30,6 +33,62 @@ module Restforce
30
33
  client.bind 'transport:up' do
31
34
  Restforce.log "[COMETD UP]"
32
35
  end
36
+
37
+ client.add_extension ReplayExtension.new(replay_handlers)
38
+ end
39
+ end
40
+
41
+ def replay_handlers
42
+ @_replay_handlers ||= {}
43
+ end
44
+
45
+ class ReplayExtension
46
+ def initialize(replay_handlers)
47
+ @replay_handlers = replay_handlers
48
+ end
49
+
50
+ def incoming(message, callback)
51
+ callback.call(message).tap do
52
+ channel = message.fetch('channel').gsub('/topic/', '')
53
+ replay_id = message.fetch('data', {}).fetch('event', {})['replayId']
54
+
55
+ handler = @replay_handlers[channel]
56
+ if !replay_id.nil? && !handler.nil? && handler.respond_to?(:[]=)
57
+ # remember the last replay_id for this channel
58
+ handler[channel] = replay_id
59
+ end
60
+ end
61
+ end
62
+
63
+ def outgoing(message, callback)
64
+ # Leave non-subscribe messages alone
65
+ return callback.call(message) unless message['channel'] == '/meta/subscribe'
66
+
67
+ channel = message['subscription'].gsub('/topic/', '')
68
+
69
+ # Set the replay value for the channel
70
+ message['ext'] ||= {}
71
+ message['ext']['replay'] = {
72
+ "/topic/#{channel}" => replay_id(channel)
73
+ }
74
+
75
+ # Carry on and send the message to the server
76
+ callback.call message
77
+ end
78
+
79
+ private
80
+
81
+ def replay_id(channel)
82
+ handler = @replay_handlers[channel]
83
+ if handler.is_a?(Integer)
84
+ handler # treat it as a scalar
85
+ elsif handler.respond_to?(:[])
86
+ # Ask for the latest replayId for this channel
87
+ handler[channel]
88
+ else
89
+ # Just pass it along
90
+ handler
91
+ end
33
92
  end
34
93
  end
35
94
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  module Concerns
3
5
  module Verbs
@@ -34,7 +36,7 @@ module Restforce
34
36
  begin
35
37
  connection.send(verb, *args, &block)
36
38
  rescue Restforce::UnauthorizedError
37
- if retries > 0
39
+ if retries.positive?
38
40
  retries -= 1
39
41
  connection.url_prefix = options[:instance_url]
40
42
  retry
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
2
4
 
3
5
  module Restforce
@@ -32,6 +34,7 @@ module Restforce
32
34
 
33
35
  def log(message)
34
36
  return unless Restforce.log?
37
+
35
38
  configuration.logger.send(configuration.log_level, message)
36
39
  end
37
40
  end
@@ -60,7 +63,7 @@ module Restforce
60
63
  private
61
64
 
62
65
  attr_reader :default
63
- alias_method :default_provided?, :default
66
+ alias default_provided? default
64
67
 
65
68
  def write_attribute
66
69
  configuration.send :attr_accessor, name
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  module Data
3
5
  class Client < AbstractClient
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  class Document < Restforce::SObject
3
5
  # Public: Returns the body of the document.
@@ -15,6 +17,7 @@ module Restforce
15
17
 
16
18
  def ensure_body
17
19
  return true if self.Body?
20
+
18
21
  raise 'You need to query the Body for the record first.'
19
22
  end
20
23
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'hashie/mash'
2
4
 
3
5
  module Restforce
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  # Base class that all middleware can extend. Provides some convenient helper
3
5
  # functions.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Restforce
2
4
  # Faraday middleware that allows for on the fly authentication of requests.
3
5
  # When a request fails (a status of 401 is returned), the middleware
@@ -31,9 +33,7 @@ module Restforce
31
33
  @options[:instance_url] = response.body['instance_url']
32
34
  @options[:oauth_token] = response.body['access_token']
33
35
 
34
- if @options[:authentication_callback]
35
- @options[:authentication_callback].call(response.body)
36
- end
36
+ @options[:authentication_callback]&.call(response.body)
37
37
 
38
38
  response.body
39
39
  end
@@ -50,9 +50,11 @@ module Restforce
50
50
  builder.use Restforce::Middleware::Mashify, nil, @options
51
51
  builder.response :json
52
52
 
53
- builder.use Restforce::Middleware::Logger,
54
- Restforce.configuration.logger,
55
- @options if Restforce.log?
53
+ if Restforce.log?
54
+ builder.use Restforce::Middleware::Logger,
55
+ Restforce.configuration.logger,
56
+ @options
57
+ end
56
58
 
57
59
  builder.adapter @options[:adapter]
58
60
  end