zuora-ruby 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7f2609823dc2d907d854ef7c950b7a683f5795b2
4
- data.tar.gz: 06c402c7774279668eb3891085eb3d9c7c72bf87
3
+ metadata.gz: 851d0abfff60f962e9325f3f2640b98e26757c9c
4
+ data.tar.gz: f45b66d5be58bef7ee808bb850d4ac3fd3a95990
5
5
  SHA512:
6
- metadata.gz: 075799410c0f4b511ade42eded914a5d5287592297b77558f0ccd5f1b288893a31d9c160fb3a9952d34536228a656f7ab9c92f985e2e578bd8b945a6ba4e40b0
7
- data.tar.gz: cd675321a91dfae484fc72447cca0378a74ab6b129b65c6d8e7c329d41cd688a70706d1624cc5b7951206c5417f17ba68f092c81c6b4435aa35f3a9ade545b19
6
+ metadata.gz: 98234b223c83ab68a662eb5dfd48f5fd827e69375cb142d101dc0570538212a674fbe9961710671644cee8494b14ab42e1b2b122c0bf22094a1a9fd54721626d
7
+ data.tar.gz: 1221da190beb70589a3260e0af9797d48f8b603a58e60ada3d5b6d4c803d3869c3761a540323fafb9376697a8121ff19a9e6a9647605eadfe17f2b61119dec1b
Binary file
@@ -0,0 +1,3 @@
1
+ ruby:
2
+ enabled: true
3
+ config_file: .rubocop.yml
data/README.md CHANGED
@@ -1,29 +1,67 @@
1
1
  [![Circle CI](https://circleci.com/gh/contactually/zuora-ruby.svg?style=shield&circle-token=808be5d625e91e331bedb37a2fe94412bb3bc15e)](https://circleci.com/gh/contactually/zuora-ruby)
2
- [![Code Climate](https://codeclimate.com/repos/569444dfa3d810003a00313f/badges/416bae00acf65d690efe/gpa.svg)](https://codeclimate.com/repos/569444dfa3d810003a00313f/feed)
3
- [![Test Coverage](https://codeclimate.com/repos/569444dfa3d810003a00313f/badges/416bae00acf65d690efe/coverage.svg)](https://codeclimate.com/repos/569444dfa3d810003a00313f/coverage)
2
+ [![Code Climate](https://codeclimate.com/repos/5706a3fb4f15bd726100652d/badges/4e4615baaec76fd16535/gpa.svg)](https://codeclimate.com/repos/569444dfa3d810003a00313f/feed)
3
+ [![Test Coverage](https://codeclimate.com/repos/5706a3fb4f15bd726100652d/badges/4e4615baaec76fd16535/coverage.svg)](https://codeclimate.com/repos/569444dfa3d810003a00313f/coverage)
4
4
 
5
- # Zuora SOAP API Client
5
+ # Zuora SOAP and REST API Client
6
6
 
7
7
  ## Features
8
- * HTTP client to Zuora SOAP API
8
+ * HTTP client to Zuora SOAP and REST API
9
9
  * Authentication and session storage
10
10
  * SOAP XML request constructors from Ruby data
11
11
  * Light validation of top-level forms; field-level validation delegated to Zuora's responses.
12
12
 
13
13
  ## Usage
14
14
 
15
-
16
15
  ### Client
17
16
 
18
- Create a client
17
+ Creating a client to both SOAP and REST API is easy:
19
18
  ```ruby
20
19
  client = Zuora::Client.new(<username>, <password>)
21
20
  ```
21
+ This will cache the login credentials (don't worry, they're excluded from being logged). Upon using methods requiring SOAP or REST client, that client is lazily authenticated against the respective API. The resulting session is cached and used in subsequent requests.
22
22
 
23
+ It's possible to use the clients directly:
24
+ ```ruby
25
+ soap_client = Zuora::Soap::Client.new(<username>, <password>)
26
+ rest_client = Zuora::Rest::Client.new(<username>, <password>)
27
+ ```
23
28
 
24
- Execute a SOAP request. Currently only `:create` and `:subscribe` are supported
29
+ ### SOAP Calls
30
+ Soap calls are made using the `call!` method. The argument structure varies depending on the SOAP method. See specs for exact interfaces.
25
31
 
26
- #### Create Example
32
+ ```ruby
33
+ client.call! :create, type: :Invoice, objects: [{...}, {...}]
34
+ client.call! :update, type: :Invoice, objects: [{...}, {...}]
35
+ client.call! :delete, type: :Invoice, ids: [{...}, {...}]
36
+ client.call! :generate, objects: [{...}, {...}]
37
+ client.call! :query, "SELECT Notes FROM Account WHERE id = '1'"
38
+ client.call! :query, [:notes], :Account, {id: 1}
39
+ client.call! :amend,
40
+ amendments: {...},
41
+ amend_options: {...},
42
+ preview_options: {}
43
+ client.call! :subscribe,
44
+ payment_method: {...}
45
+ bill_to_contact: {...}
46
+ sold_to_contact: {...}
47
+ subscribe_options: {...}
48
+ subscription: {...}
49
+ rate_plan: {...}
50
+ ```
51
+
52
+ SOAP requests return a `Zuora::Response` object that parses the XML response into Ruby data via the `#to_h` method. The raw request is available via the `#raw` method.
53
+
54
+ ### REST
55
+ ```ruby
56
+ client.get('/rest/v1/accounts/1')
57
+ client.delete('/rest/v1/accounts/1')
58
+ client.post('/rest/v1/accounts', notes: 'hello')
59
+ client.put('/rest/v1/accounts/1', id: 1, notes: 'world')
60
+ ```
61
+
62
+ REST requests return a Farraday::Response object, which has a `body` and `status`. See [Farraday](https://github.com/lostisland/faraday) docs for details.
63
+
64
+ #### SOAP Create Example
27
65
 
28
66
  ```ruby
29
67
  response = client.call! :create,
@@ -101,7 +139,25 @@ response = client.call! :subscribe,
101
139
  ```
102
140
 
103
141
  # Changelog
104
- * **[0.1.0 - 2016-01-12]** Initial release
142
+ * **[0.5.0 2016-05-12]** Uniform REST and SOAP client
143
+ - Generalizes the client to work for both REST and SOAP APIs. In practice, both are useful to access the gamut of Zuora's operations. SOAP is better for fine-grained control, while REST is larger-grained and shifts the burden of transactions onto Zuora for certain operations.
144
+ - Adds integration specs to cover REST GET, POST, PUT, DELETE
145
+ - Errors are thrown for unsuccessful responses
146
+ - Prevents credentials from being logged
147
+ - SOAP Query call: now with arity-1 and arity-3 versions, pass a ZOQL query as string or as data. See docs and specs for details.
148
+ - Add support for queryMore for result sets greater than 2000 in size
149
+
150
+ * **[0.4.0 2016-02-10]** Improves interface and feedback loop between developer and Zuora servers.
151
+ - Allow flexible submission of parameters to the API. Let Zuora's API handle validation instead of performing in the client.
152
+ - Adds integration specs to cover base functionality
153
+ - Adds exception raising to match servier-side exceptions such as missing required fields, invalid data, etc.
154
+
155
+ * **[0.3.0 2016-1-28]** Focus on SOAP API, simplify client library feature set
156
+ - Implement SOAP API Client; it provides fuller functionality than REST
157
+ - Focus on constructing + composing hash-like Ruby objects into XML SOAP requests
158
+ - No object-level validations; relies on Zuora's own responses
159
+ - See integration specs for full interface
160
+
105
161
  * **[0.2.0] - 2016-01-14]** Models
106
162
  - Refactored client to clarify logic
107
163
  - Replaces `ActiveRecord::Model` and `::Validations` with a base module that provides powerful and extensible facilities for modeling remote resources in Ruby.
@@ -114,16 +170,7 @@ response = client.call! :subscribe,
114
170
  - Adds VCR for mocking out HTTP requests
115
171
  - Adds integration specs for `Subscribe` `create!` and `update!` and `Account` `create!` and `update!`
116
172
 
117
- * **[0.3.0 2016-1-28]** Focus on SOAP API, simpify client library feature set
118
- - Implement SOAP API Client; it provides fuller functionality than REST
119
- - Focus on constructing + composing hash-like Ruby objects into XML SOAP requests
120
- - No object-level validations; relies on Zuora's own responses
121
- - See integration specs for full interface
122
-
123
- * **[0.4.0 2016-02-10]** Improves interface and feedback loop between developer and Zuora servers.
124
- - Allow flexible submission of parameters to the API. Let Zuora's API handle validation instead of performing in the client.
125
- - Adds integration specs to cover base functionality
126
- - Adds exception raising to match servier-side exceptions such as missing required fields, invalid data, etc.
173
+ * **[0.1.0 - 2016-01-12]** Initial release
127
174
 
128
175
  # Commit rights
129
176
  Anyone who has a patch accepted may request commit rights. Please do so inside the pull request post-merge.
@@ -21,12 +21,16 @@ module Zuora
21
21
  'xmlns:ns1' => 'http://api.zuora.com/',
22
22
  'xmlns:ns2' => 'http://object.api.zuora.com/'
23
23
  ).freeze
24
+
25
+ RETRY_WAITING_PERIOD = 120 # seconds
24
26
  end
25
27
 
26
28
  require_relative 'zuora/version'
27
29
  require_relative 'zuora/errors'
28
30
  require_relative 'zuora/utils/envelope'
29
31
  require_relative 'zuora/client'
32
+ require_relative 'zuora/rest'
33
+ require_relative 'zuora/soap'
30
34
  require_relative 'zuora/object'
31
35
  require_relative 'zuora/dispatcher'
32
36
  require_relative 'zuora/response'
@@ -39,5 +43,6 @@ require_relative 'zuora/calls/delete'
39
43
  require_relative 'zuora/calls/generate'
40
44
  require_relative 'zuora/calls/login'
41
45
  require_relative 'zuora/calls/query'
46
+ require_relative 'zuora/calls/query_more'
42
47
  require_relative 'zuora/calls/subscribe'
43
48
  require_relative 'zuora/calls/update'
@@ -11,9 +11,11 @@ module Zuora
11
11
  lambda do |builder|
12
12
  builder[:api].amend do
13
13
  builder[:api].requests do
14
- build_object builder, :amendments, :obj
15
- build_object builder, :amend_options, :api
16
- build_object builder, :preview_options, :api
14
+ Array.wrap(amendments).each do |amendment|
15
+ build_object builder, :amendments, amendment, :obj
16
+ end
17
+ build_object builder, :amend_options, amend_options, :api
18
+ build_object builder, :preview_options, preview_options, :api
17
19
  end
18
20
  end
19
21
  end
@@ -55,8 +57,7 @@ module Zuora
55
57
  # @param [Symbol] property_name - name of a property on this object
56
58
  # @param [Symbol] child_ns - namespace of child node fields
57
59
  # @return nil
58
- def build_object(builder, property_name, child_ns)
59
- object = send property_name
60
+ def build_object(builder, property_name, object, child_ns)
60
61
  fail 'Objects must respond to each' unless object.respond_to?(:each)
61
62
  object_name = Zuora::Utils::Envelope.to_zuora_key property_name
62
63
  builder[:api].send(object_name) do
@@ -1,15 +1,54 @@
1
1
  module Zuora
2
2
  module Calls
3
3
  class Query < Hashie::Dash
4
- def initialize(query_string)
5
- @query_string = query_string
4
+ # This constructor has two arities.
5
+ # Arity 1: provide a raw ZOQL query string
6
+ # .new 'SELECT Id FROM Account WHERE Id = '1')
7
+
8
+ # Arity 2: provide simple query components
9
+ # .new [:id], :account, { :id => 1 }) will be transformed into query above
10
+
11
+ # @param [String|Array] select - query statement or field name sym array
12
+ # @param [Symbol|Nil] from - table name symbol
13
+ # @param [Symbol|Nil] where - hash of equalities for where clauses
14
+ # Operations: only = is supported
15
+ # Custom field names are supported: some_field__c => SomeField__c
16
+ # @return [Zuora::Calls:Query]
17
+ def initialize(select, from = nil, where = nil)
18
+ @query_string = if select.is_a? Array
19
+ query_to_string(select, from, where)
20
+ else
21
+ select
22
+ end
6
23
  end
7
24
 
25
+ # @return [Callable]
8
26
  def xml_builder
9
27
  lambda do |builder|
10
28
  builder[:api].query { builder[:api].queryString(@query_string) }
11
29
  end
12
30
  end
31
+
32
+ private
33
+
34
+ # @param [Array] fields
35
+ # @param [Symbol] table
36
+ # @param [Hash] conditions
37
+ def query_to_string(fields, table, conditions)
38
+ fail 'Fields must be an Array' unless fields.is_a?(Array)
39
+ fail 'Table must respond to :to_sym' unless table.respond_to?(:to_sym)
40
+ fail 'Conditions must be Array' if fields && !fields.is_a?(Array)
41
+
42
+ key_fn = ->(key) { Zuora::Utils::Envelope.to_zuora_key(key) }
43
+
44
+ select = fields.map { |field| key_fn[field] }.join(', ').to_s
45
+ from = table.to_s
46
+ where = 'WHERE ' + conditions.map do |key, value|
47
+ "#{key_fn[key]} = '#{value}'"
48
+ end.join(' AND ') if conditions
49
+
50
+ "SELECT #{select} FROM #{from} #{where}".strip.squeeze(' ')
51
+ end
13
52
  end
14
53
  end
15
54
  end
@@ -0,0 +1,22 @@
1
+ module Zuora
2
+ module Calls
3
+ class QueryMore < Hashie::Dash
4
+ # After executing a query, often time a query_locator is returned when
5
+ # there are more records than Zuora can respond with in a single response.
6
+ # The default batch size is 2000. You can use a combination of query and
7
+ # query_more calls to get large quantities of data.
8
+ # @param [String] query_locator
9
+ # @return [Zuora::Calls:Query]
10
+ def initialize(query_locator)
11
+ @query_locator = query_locator
12
+ end
13
+
14
+ # @return [Callable]
15
+ def xml_builder
16
+ lambda do |builder|
17
+ builder[:api].queryMore { builder[:api].queryLocator(@query_locator) }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -6,120 +6,52 @@ module Zuora
6
6
  class Client
7
7
  attr_accessor :session_token
8
8
 
9
- SOAP_API_URI = '/apps/services/a/74.0'.freeze
10
- SESSION_TOKEN_XPATH =
11
- %w(//soapenv:Envelope soapenv:Body api:loginResponse
12
- api:result api:Session).join('/').freeze
9
+ INSTANCE_VARIABLE_LOG_BLACKLIST = [:@username, :@password].freeze
13
10
 
14
- # Creates a connection instance.
15
- # Makes an initial SOAP request to fetch session token.
16
- # Subsequent requests contain the authenticated session id
17
- # in headers.
18
- # @param [String] username
19
- # @param [String] password
20
- # @param [Boolean] sandbox
21
- # @return [Zuora::SoapClient]
22
11
  def initialize(username, password, sandbox = true)
23
12
  @username = username
24
13
  @password = password
25
14
  @sandbox = sandbox
26
15
  end
27
16
 
28
- # Makes auth request, handles response
29
- # @return [Faraday::Response]
30
- def authenticate!
31
- auth_response = call! :login,
32
- username: @username,
33
- password: @password
34
-
35
- handle_auth_response auth_response
36
- rescue Object => e
37
- raise Zuora::Errors::SoapConnectionError, e
17
+ # Delegate SOAP methods to SOAP client
18
+ def call!(*args)
19
+ soap_client.call!(*args)
38
20
  end
39
21
 
40
- # Fire a request
41
- # @param [Xml] body - an object responding to .xml
42
- # @return [Zuora::Response]
43
- def request!(body)
44
- fail 'body must support .to_xml' unless body.respond_to? :to_xml
22
+ # Delegate REST methods to REST client
23
+ [:post, :put, :get, :delete].each do |method|
24
+ define_method(method) do |*args|
25
+ rest_client.send(method, *args)
26
+ end
27
+ end
45
28
 
46
- raw_response = connection.post do |request|
47
- request.url SOAP_API_URI
48
- request.headers['Content-Type'] = 'text/xml'
49
- request.body = body.to_xml
29
+ # Like Object.to_s, except excludes BLACKLISTed instance vars
30
+ def to_s
31
+ public_vars = instance_variables.reject do |var|
32
+ INSTANCE_VARIABLE_LOG_BLACKLIST.include? var
50
33
  end
51
34
 
52
- response = Zuora::Response.new(raw_response)
35
+ public_vars.map! do |var|
36
+ "#{var}=\"#{instance_variable_get(var)}\""
37
+ end
53
38
 
54
- response.handle_errors(response.to_h)
39
+ public_vars = public_vars.join(' ')
55
40
 
56
- response
41
+ "<##{self.class}:#{object_id.to_s(8)} #{public_vars}>"
57
42
  end
58
43
 
59
- # The primary interface via which users should make SOAP requests.
60
- # client.call :create, object_name: :BillRun, data: {...}
61
- # client.call :subscribe, account: {...}, sold_to_contact: {...}
62
- # @param [Symbol] call_name - one of :create, :subscribe, :amend, :update
63
- # @return [Faraday:Response] - response
64
- def call!(call_name, *args)
65
- factory = Zuora::Dispatcher.send call_name
66
- xml_builder = factory.new(*args).xml_builder
67
- request_data = envelope_for call_name, xml_builder
68
- request! request_data
69
- end
44
+ alias inspect to_s
70
45
 
71
46
  private
72
47
 
73
- # Generate envelope for request
74
- # @param [Symbol] call_name - one of the supported calls (see #call)
75
- # @param [Callable] xml_builder_modifier - function taking a builder
76
- # @return [Nokogiri::XML::Builder]
77
- def envelope_for(call_name, xml_builder_modifier)
78
- if call_name == :login
79
- Zuora::Utils::Envelope.xml(nil, xml_builder_modifier)
80
- else
81
- Zuora::Utils::Envelope.authenticated_xml(@session_token) do |b|
82
- xml_builder_modifier.call b
83
- end
84
- end
48
+ # Lazily connects SOAP / RESTS clients when needed; memoizes results
49
+ def soap_client
50
+ @soap_client ||= Zuora::Soap::Client.new(@username, @password, @sandbox)
85
51
  end
86
52
 
87
- # Handle auth response, setting session
88
- # @params [Faraday::Response]
89
- # @return [Faraday::Response]
90
- # @throw [Zuora::Errors::InvalidCredentials]
91
- def handle_auth_response(response)
92
- if response.raw.status == 200
93
- @session_token = extract_session_token response
94
- else
95
- message = 'Unable to connect with provided credentials'
96
- fail Zuora::Errors::InvalidCredentials, message
97
- end
98
- response
99
- end
100
-
101
- # Extracts session token from response and sets instance variable
102
- # for use in subsequent requests
103
- # @param [Faraday::Response] response - response to auth request
104
- def extract_session_token(response)
105
- response.to_h.envelope.body.login_response.result.session
106
- end
107
-
108
- # Initializes a connection using api_url
109
- # @return [Faraday::Connection]
110
- def connection
111
- Faraday.new(api_url, ssl: { verify: false }) do |conn|
112
- conn.adapter Faraday.default_adapter
113
- end
114
- end
115
-
116
- # @return [String] - SOAP url based on @sandbox
117
- def api_url
118
- if @sandbox
119
- 'https://apisandbox.zuora.com/apps/services/a/74.0'
120
- else
121
- 'https://api.zuora.com/apps/services/a/74.0'
122
- end
53
+ def rest_client
54
+ @rest_client ||= Zuora::Rest::Client.new(@username, @password, @sandbox)
123
55
  end
124
56
  end
125
57
  end
@@ -27,6 +27,10 @@ module Zuora
27
27
  Zuora::Calls::Query
28
28
  end
29
29
 
30
+ def query_more
31
+ Zuora::Calls::QueryMore
32
+ end
33
+
30
34
  def delete
31
35
  Zuora::Calls::Delete
32
36
  end
@@ -27,7 +27,7 @@ module Zuora
27
27
  if value.is_a?(Hash)
28
28
  handle_errors(value)
29
29
  elsif value.is_a?(Array)
30
- value.each { |v| handle_errors(v) }
30
+ value.each { |v| handle_errors(v) unless v.is_a?(String) }
31
31
  elsif error?(key, value)
32
32
  errors << value
33
33
  end
@@ -0,0 +1,21 @@
1
+ module Zuora
2
+ module Rest
3
+ API_URL = 'https://api.zuora.com/rest/v1/'.freeze
4
+ SANDBOX_URL = 'https://apisandbox-api.zuora.com/rest/v1/'.freeze
5
+
6
+ # Unable to connect. Check username / password
7
+ ConnectionError = Class.new StandardError
8
+
9
+ # Non-success response
10
+ class ErrorResponse < StandardError
11
+ attr_reader :response
12
+
13
+ def initialize(message = nil, response = nil)
14
+ super(message)
15
+ @response = response
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ require_relative 'rest/client'
@@ -0,0 +1,132 @@
1
+ # encoding: utf-8
2
+ require 'faraday'
3
+ require 'faraday_middleware'
4
+ require 'json'
5
+
6
+ module Zuora
7
+ module Rest
8
+ class Client
9
+ attr_reader :connection
10
+
11
+ # Creates a connection instance.
12
+ # Makes an initial HTTP request to fetch session token.
13
+ # Subsequent requests made with .get, .post, and .put
14
+ # contain the authenticated session id in their headers.
15
+ # @param [String] username
16
+ # @param [String] password
17
+ # @param [Boolean] sandbox
18
+ # @return [Zuora::Client] with .connection, .put, .post
19
+ def initialize(username, password, sandbox = false)
20
+ base_url = api_url sandbox
21
+ conn = connection base_url
22
+
23
+ response = conn.post do |req|
24
+ set_auth_request_headers! req, username, password
25
+ end
26
+
27
+ case response.status
28
+ when 200
29
+ @auth_cookie = response.headers['set-cookie'].split(' ')[0]
30
+ @connection = conn
31
+ when 429
32
+ sleep(Zuora::RETRY_WAITING_PERIOD)
33
+ return initialize(username, password, sandbox)
34
+ else
35
+ fail Zuora::Rest::ConnectionError, response.body['reasons']
36
+ end
37
+ end
38
+
39
+ # @param [String] url - URL of request
40
+ # @return [Faraday::Response] A response, with .headers, .status & .body
41
+ [:get, :delete].each do |method|
42
+ define_method(method) do |url|
43
+ response = @connection.send(method) do |req|
44
+ set_request_headers! req, url
45
+ end
46
+
47
+ # Handle rate limiting
48
+ return handle_rate_limiting(method, url) if response.status == 429
49
+
50
+ fail_or_response(response)
51
+ end
52
+ end
53
+
54
+ # @param [String] url - URL for HTTP POST / PUT request
55
+ # @param [Params] params - Data to be sent in request body
56
+ # @return [Faraday::Response] A response, with .headers, .status & .body
57
+ [:post, :put].each do |method|
58
+ define_method method do |url, params|
59
+ response = @connection.send(method) do |req|
60
+ set_request_headers! req, url
61
+ req.body = JSON.generate params
62
+ end
63
+
64
+ # Handle rate limiting
65
+ if response.status == 429
66
+ return handle_rate_limiting(method, url, params)
67
+ end
68
+
69
+ fail_or_response(response)
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ # @param [String] method
76
+ # @param [String] url
77
+ # @param [Hash] params
78
+ def handle_rate_limiting(method, url, params = nil)
79
+ sleep(Zuora::RETRY_WAITING_PERIOD)
80
+ if params.present?
81
+ send(method, url, params)
82
+ else
83
+ send(method, url)
84
+ end
85
+ end
86
+
87
+ # @param [Faraday::Response] response
88
+ # @throw [ErrorResponse] if unsuccessful
89
+ # @return [Faraday::Response]
90
+ def fail_or_response(response)
91
+ success = response.body['success'] && response.status == 200
92
+ fail(ErrorResponse.new('Non-200', response)) unless success
93
+ response
94
+ end
95
+
96
+ # @param [Faraday::Request] req - Faraday::Request builder
97
+ # @param [String] username
98
+ # @param [String] password
99
+ def set_auth_request_headers!(req, username, password)
100
+ req.url '/rest/v1/connections'
101
+ req.headers['apiAccessKeyId'] = username
102
+ req.headers['apiSecretAccessKey'] = password
103
+ req.headers['Content-Type'] = 'application/json'
104
+ end
105
+
106
+ # @param [Faraday::Request] request - Faraday Request builder
107
+ # @param [String] url - Relative URL for HTTP request
108
+ def set_request_headers!(request, url)
109
+ request.url url
110
+ request.headers['Content-Type'] = 'application/json'
111
+ request.headers['Cookie'] = @auth_cookie
112
+ end
113
+
114
+ # @param [String] url
115
+ # @return [Faraday::Client]
116
+ def connection(url)
117
+ Faraday.new(url, ssl: { verify: false }) do |conn|
118
+ conn.request :json
119
+ conn.response :json, content_type: /\bjson$/
120
+ conn.use :instrumentation
121
+ conn.adapter Faraday.default_adapter
122
+ end
123
+ end
124
+
125
+ # @param [Boolean] sandbox - Use the sandbox url?
126
+ # @return [String] url
127
+ def api_url(sandbox)
128
+ sandbox ? Zuora::Rest::SANDBOX_URL : Zuora::Rest::API_URL
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,7 @@
1
+ module Zuora
2
+ module Soap
3
+ end
4
+ end
5
+
6
+ require_relative 'soap/client'
7
+ require_relative 'soap/z_object'
@@ -0,0 +1,141 @@
1
+ module Zuora
2
+ module Soap
3
+ class Client
4
+ attr_reader :session_token
5
+
6
+ SOAP_API_URI = '/apps/services/a/74.0'.freeze
7
+ SESSION_TOKEN_XPATH =
8
+ %w(//soapenv:Envelope soapenv:Body api:loginResponse
9
+ api:result api:Session).join('/').freeze
10
+
11
+ # Creates a connection instance.
12
+ # Makes an initial SOAP request to fetch session token.
13
+ # Subsequent requests contain the authenticated session id
14
+ # in headers.
15
+ # @param [String] username
16
+ # @param [String] password
17
+ # @param [Boolean] sandbox
18
+ # @return [Zuora::SoapClient]
19
+ def initialize(username, password, sandbox = true)
20
+ @sandbox = sandbox
21
+ authenticate!(username, password)
22
+ end
23
+
24
+ # Fire a request
25
+ # @param [Xml] body - an object responding to .xml
26
+ # @return [Zuora::Response]
27
+ def request!(body)
28
+ fail 'body must support .to_xml' unless body.respond_to? :to_xml
29
+
30
+ raw_response = connection.post do |request|
31
+ request.url SOAP_API_URI
32
+ request.headers['Content-Type'] = 'text/xml'
33
+ request.body = body.to_xml
34
+ end
35
+
36
+ # Handle rate limiting
37
+ return handle_rate_limiting(body) if raw_response.status == 429
38
+
39
+ response = Zuora::Response.new(raw_response)
40
+ begin
41
+ response.handle_errors(response.to_h)
42
+ rescue => e
43
+ return handle_lock_competition(e, body)
44
+ end
45
+ response
46
+ end
47
+
48
+ # The primary interface via which users should make SOAP requests.
49
+ # client.call :create, object_name: :BillRun, data: {...}
50
+ # client.call :subscribe, account: {...}, sold_to_contact: {...}
51
+ # @param [Symbol] call_name - one of :create, :subscribe, :amend, :update
52
+ # @return [Faraday:Response] - response
53
+ def call!(call_name, *args)
54
+ factory = Zuora::Dispatcher.send call_name
55
+ xml_builder = factory.new(*args).xml_builder
56
+ request_data = envelope_for call_name, xml_builder
57
+ request! request_data
58
+ end
59
+
60
+ private
61
+
62
+ # @param [Xml] body
63
+ # @return [Zuora::Response]
64
+ def handle_rate_limiting(body)
65
+ sleep(Zuora::RETRY_WAITING_PERIOD)
66
+ request!(body)
67
+ end
68
+
69
+ def handle_lock_competition(error, body)
70
+ if error.message =~ /(Operation failed due to a lock competition)/i
71
+ handle_rate_limiting(body)
72
+ else
73
+ fail error
74
+ end
75
+ end
76
+
77
+ # Makes auth request, handles response
78
+ # @return [Faraday::Response]
79
+ # @param [String] username
80
+ # @param [String] password
81
+ def authenticate!(username, password)
82
+ auth_response = call! :login,
83
+ username: username,
84
+ password: password
85
+
86
+ handle_auth_response auth_response
87
+ rescue Object => e
88
+ raise Zuora::Errors::SoapConnectionError, e
89
+ end
90
+
91
+ # Generate envelope for request
92
+ # @param [Symbol] call_name - one of the supported calls (see #call)
93
+ # @param [Callable] xml_builder_modifier - function taking a builder
94
+ # @return [Nokogiri::XML::Builder]
95
+ def envelope_for(call_name, xml_builder_modifier)
96
+ if call_name == :login
97
+ Zuora::Utils::Envelope.xml(nil, xml_builder_modifier)
98
+ else
99
+ Zuora::Utils::Envelope.authenticated_xml(@session_token) do |b|
100
+ xml_builder_modifier.call b
101
+ end
102
+ end
103
+ end
104
+
105
+ # Handle auth response, setting session
106
+ # @params [Faraday::Response]
107
+ # @return [Faraday::Response]
108
+ # @throw [Zuora::Errors::InvalidCredentials]
109
+ def handle_auth_response(response)
110
+ if response.raw.status == 200
111
+ @session_token = extract_session_token response
112
+ else
113
+ message = 'Unable to connect with provided credentials'
114
+ fail Zuora::Errors::InvalidCredentials, message
115
+ end
116
+ response
117
+ end
118
+
119
+ # Extracts session token from response and sets instance variable
120
+ # for use in subsequent requests
121
+ # @param [Faraday::Response] response - response to auth request
122
+ def extract_session_token(response)
123
+ response.to_h.envelope.body.login_response.result.session
124
+ end
125
+
126
+ # Initializes a connection using api_url
127
+ # @return [Faraday::Connection]
128
+ def connection
129
+ Faraday.new(api_url, ssl: { verify: false }) do |conn|
130
+ conn.adapter Faraday.default_adapter
131
+ end
132
+ end
133
+
134
+ # @return [String] - SOAP url based on @sandbox
135
+ def api_url
136
+ host_prefix = @sandbox ? 'sandbox' : ''
137
+ "https://api#{host_prefix}.zuora.com/apps/services/a/74.0"
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,18 @@
1
+ module Zuora
2
+ module Soap
3
+ class ZObject
4
+ extend Forwardable
5
+
6
+ attr_reader :type, :fields
7
+
8
+ # @params [Symbol] - name of ZObject
9
+ # @params [Hash] - a hash of fields
10
+ def initialize(type, fields)
11
+ @type = type
12
+ @fields = fields
13
+ end
14
+
15
+ def_delegators :@fields, :each, :map, :reduce, :inspect, :to_i
16
+ end
17
+ end
18
+ end
@@ -8,12 +8,8 @@ module Zuora
8
8
  def self.xml(header, body)
9
9
  Nokogiri::XML::Builder.new do |builder|
10
10
  builder[:soapenv].Envelope(Zuora::NAMESPACES) do
11
- builder[:soapenv].Header do
12
- header.call builder
13
- end if header
14
- builder[:soapenv].Body do
15
- body.call builder
16
- end if body
11
+ builder[:soapenv].Header { header.call builder } if header
12
+ builder[:soapenv].Body { body.call builder } if body
17
13
  end
18
14
  end
19
15
  end
@@ -26,24 +22,49 @@ module Zuora
26
22
  fail failure_message unless token.present?
27
23
 
28
24
  header = lambda do |builder|
29
- builder[:api].SessionHeader do
30
- builder[:api].session(token)
31
- end
25
+ builder[:api].SessionHeader { builder[:api].session(token) }
32
26
  builder
33
27
  end
34
28
 
35
29
  xml(header, body)
36
30
  end
37
31
 
38
- # Builds multiple fields
32
+ # Builds one field using key and value. Treats value differently:
33
+ # - Hash: recursively builds fields
34
+ # - ZObject: builds a nested z object, building fields inside
35
+ # - Nil: does nothing
36
+ # - Else: builds the field node
37
+ # @param [Nokogiri::XML::Builder] builder
38
+ # @param [Symbol] namespace
39
+ # @param [Hash] key
40
+ # @param [Hash|Zuora::Soap::ZObject|NilClass|Object] value
41
+ # @return nil
42
+ def self.build_field(builder, namespace, key, value)
43
+ zuora_field_name = to_zuora_key(key)
44
+ build_fields_thunk = -> { build_fields builder, namespace, value }
45
+ case value
46
+ when Hash
47
+ builder[namespace].send(zuora_field_name) { build_fields_thunk[] }
48
+ when Zuora::Soap::ZObject
49
+ zuora_type = to_zuora_key(value.type)
50
+ xsi = { 'xsi:type' => "obj:#{zuora_type}" }
51
+ builder[:api].send(zuora_field_name) do
52
+ builder[:api].send(zuora_type, xsi) { build_fields_thunk[] }
53
+ end
54
+ when NilClass
55
+ else
56
+ builder[namespace].send(zuora_field_name, value)
57
+ end
58
+ end
59
+
60
+ # Builds multiple fields in given object
39
61
  # @param [Nokogiri::XML::Builder] builder
40
62
  # @param [Symbol] namespace
41
63
  # @param [Hash] object
42
64
  # @return nil
43
65
  def self.build_fields(builder, namespace, object = {})
44
66
  object.each do |key, value|
45
- zuora_field_name = to_zuora_key(key)
46
- builder[namespace].send(zuora_field_name, value) unless value.nil?
67
+ build_field builder, namespace, key, value
47
68
  end
48
69
  end
49
70
 
@@ -1,3 +1,3 @@
1
1
  module Zuora
2
- VERSION = '0.4.0'.freeze
2
+ VERSION = '0.5.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zuora-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Contactually Engineering
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-02-10 00:00:00.000000000 Z
11
+ date: 2016-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -269,7 +269,9 @@ executables: []
269
269
  extensions: []
270
270
  extra_rdoc_files: []
271
271
  files:
272
+ - ".DS_Store"
272
273
  - ".gitignore"
274
+ - ".hound.yml"
273
275
  - ".rspec"
274
276
  - ".rubocop.yml"
275
277
  - ".ruby-version"
@@ -289,6 +291,7 @@ files:
289
291
  - lib/zuora/calls/generate.rb
290
292
  - lib/zuora/calls/login.rb
291
293
  - lib/zuora/calls/query.rb
294
+ - lib/zuora/calls/query_more.rb
292
295
  - lib/zuora/calls/subscribe.rb
293
296
  - lib/zuora/calls/update.rb
294
297
  - lib/zuora/calls/upsert.rb
@@ -297,6 +300,11 @@ files:
297
300
  - lib/zuora/errors.rb
298
301
  - lib/zuora/object.rb
299
302
  - lib/zuora/response.rb
303
+ - lib/zuora/rest.rb
304
+ - lib/zuora/rest/client.rb
305
+ - lib/zuora/soap.rb
306
+ - lib/zuora/soap/client.rb
307
+ - lib/zuora/soap/z_object.rb
300
308
  - lib/zuora/utils/envelope.rb
301
309
  - lib/zuora/version.rb
302
310
  - zuora_ruby.gemspec