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 +4 -4
- data/.DS_Store +0 -0
- data/.hound.yml +3 -0
- data/README.md +66 -19
- data/lib/zuora.rb +5 -0
- data/lib/zuora/calls/amend.rb +6 -5
- data/lib/zuora/calls/query.rb +41 -2
- data/lib/zuora/calls/query_more.rb +22 -0
- data/lib/zuora/client.rb +25 -93
- data/lib/zuora/dispatcher.rb +4 -0
- data/lib/zuora/response.rb +1 -1
- data/lib/zuora/rest.rb +21 -0
- data/lib/zuora/rest/client.rb +132 -0
- data/lib/zuora/soap.rb +7 -0
- data/lib/zuora/soap/client.rb +141 -0
- data/lib/zuora/soap/z_object.rb +18 -0
- data/lib/zuora/utils/envelope.rb +33 -12
- data/lib/zuora/version.rb +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 851d0abfff60f962e9325f3f2640b98e26757c9c
|
4
|
+
data.tar.gz: f45b66d5be58bef7ee808bb850d4ac3fd3a95990
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 98234b223c83ab68a662eb5dfd48f5fd827e69375cb142d101dc0570538212a674fbe9961710671644cee8494b14ab42e1b2b122c0bf22094a1a9fd54721626d
|
7
|
+
data.tar.gz: 1221da190beb70589a3260e0af9797d48f8b603a58e60ada3d5b6d4c803d3869c3761a540323fafb9376697a8121ff19a9e6a9647605eadfe17f2b61119dec1b
|
data/.DS_Store
ADDED
Binary file
|
data/.hound.yml
ADDED
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/
|
3
|
-
[![Test Coverage](https://codeclimate.com/repos/
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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.
|
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.
|
data/lib/zuora.rb
CHANGED
@@ -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'
|
data/lib/zuora/calls/amend.rb
CHANGED
@@ -11,9 +11,11 @@ module Zuora
|
|
11
11
|
lambda do |builder|
|
12
12
|
builder[:api].amend do
|
13
13
|
builder[:api].requests do
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
data/lib/zuora/calls/query.rb
CHANGED
@@ -1,15 +1,54 @@
|
|
1
1
|
module Zuora
|
2
2
|
module Calls
|
3
3
|
class Query < Hashie::Dash
|
4
|
-
|
5
|
-
|
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
|
data/lib/zuora/client.rb
CHANGED
@@ -6,120 +6,52 @@ module Zuora
|
|
6
6
|
class Client
|
7
7
|
attr_accessor :session_token
|
8
8
|
|
9
|
-
|
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
|
-
#
|
29
|
-
|
30
|
-
|
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
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
35
|
+
public_vars.map! do |var|
|
36
|
+
"#{var}=\"#{instance_variable_get(var)}\""
|
37
|
+
end
|
53
38
|
|
54
|
-
|
39
|
+
public_vars = public_vars.join(' ')
|
55
40
|
|
56
|
-
|
41
|
+
"<##{self.class}:#{object_id.to_s(8)} #{public_vars}>"
|
57
42
|
end
|
58
43
|
|
59
|
-
|
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
|
-
#
|
74
|
-
|
75
|
-
|
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
|
-
|
88
|
-
|
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
|
data/lib/zuora/dispatcher.rb
CHANGED
data/lib/zuora/response.rb
CHANGED
data/lib/zuora/rest.rb
ADDED
@@ -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
|
data/lib/zuora/soap.rb
ADDED
@@ -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
|
data/lib/zuora/utils/envelope.rb
CHANGED
@@ -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
|
12
|
-
|
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
|
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
|
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
|
-
|
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
|
|
data/lib/zuora/version.rb
CHANGED
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
|
+
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-
|
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
|