restforce 2.5.4 → 4.0.0

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.
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