quest_back 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +147 -0
- data/Rakefile +1 -0
- data/config.example.yml +3 -0
- data/lib/quest_back/api.rb +310 -0
- data/lib/quest_back/configuration.rb +34 -0
- data/lib/quest_back/debug_observer.rb +18 -0
- data/lib/quest_back/error.rb +7 -0
- data/lib/quest_back/response.rb +45 -0
- data/lib/quest_back/version.rb +3 -0
- data/lib/quest_back.rb +57 -0
- data/quest_back.gemspec +27 -0
- data/spec/fixtures/add_email_invitees_success.xml +7 -0
- data/spec/fixtures/add_respondents_data_success.xml +7 -0
- data/spec/fixtures/get_language_list_success.xml +588 -0
- data/spec/fixtures/get_quests_multiple_response_success.xml +50 -0
- data/spec/fixtures/get_quests_success.xml +32 -0
- data/spec/fixtures/quest_back.wsdl +165 -0
- data/spec/fixtures/test_connection_failure.xml +15 -0
- data/spec/fixtures/test_connection_success.xml +8 -0
- data/spec/quest_back/api_spec.rb +257 -0
- data/spec/quest_back/configuration_spec.rb +17 -0
- data/spec/quest_back/response_spec.rb +52 -0
- data/spec/spec_helper.rb +35 -0
- metadata +158 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 71ac294356aae1fe3423d0cba13cb705a1245cfd
|
4
|
+
data.tar.gz: 99bf75d03a7984badd0291ba474d27e07b9796fd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 83d3d479391da098c8c62d9bc1d157c0fe14708c1ad320cb76d61605519b3404ce133648a8a38007c00cc9a436882f8f052d269786c05590e8b3ab56ce180d91
|
7
|
+
data.tar.gz: f030b1fa5a1f02645e39386cc630cde2c08d5f5e06d2a7dd02938bb941c15905192df38c24bfe467ae3bec4c645cdffb16d5a0fb299977aa8d5c223cfe7d9299
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Thorbjørn Hermansen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# QuestBack
|
2
|
+
|
3
|
+
Simply Ruby client for QuestBack's SOAP API.
|
4
|
+
|
5
|
+
### WARNING
|
6
|
+
|
7
|
+
This gem is not complete and may lack many functions provided by QuestBack.
|
8
|
+
Please feel free to contribute and make pull requests.
|
9
|
+
|
10
|
+
It is also very simplistic, only using simple hashes for both sending in arguments to the API and returning responses.
|
11
|
+
The hash you send as argument to operation methods is more or less sent on to QuestBack.
|
12
|
+
Maybe this will change in the future with real objects sent in to the API.
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
gem 'quest_back'
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install quest_back
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
### Get access - test client in console
|
34
|
+
|
35
|
+
1. Go to https://response.questback.com/soapdocs/integration/ and request white listing of the IP you will make requests from.
|
36
|
+
2. Sign in to QuestBack. Go to your account's page and fill in "Integration username and password".
|
37
|
+
If you have no fields under Integration Information you have to contact QuestBack to get access.
|
38
|
+
3. Copy config.example.yml to config.yml and insert your username and password.
|
39
|
+
4. `QuestBack.conf!` to load config.yml as default config.
|
40
|
+
5. `QuestBack::Client.new.test_connection` to make a test connection API call. On successful connection this returns string with current namespace of integration library.
|
41
|
+
|
42
|
+
|
43
|
+
### Example of usage
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
# Read quests
|
47
|
+
api = QuestBack::Api.new
|
48
|
+
response = api.get_quests
|
49
|
+
irb(main):005:0> response.results
|
50
|
+
=> [
|
51
|
+
{
|
52
|
+
:quest_id=>"4567668",
|
53
|
+
:security_lock=>"m0pI8orKJp",
|
54
|
+
:quest_title=>"Skalars spørreundersøkelse",
|
55
|
+
...
|
56
|
+
},
|
57
|
+
{
|
58
|
+
...
|
59
|
+
}
|
60
|
+
]
|
61
|
+
|
62
|
+
|
63
|
+
# Add email invitees
|
64
|
+
response = api.add_email_invitees(
|
65
|
+
quest_info: {quest_id: 4567668, security_lock: 'm0pI8orKJp'},
|
66
|
+
emails: ['inviso@skalar.no', 'th@skalar.no'],
|
67
|
+
sendduplicate: true
|
68
|
+
)
|
69
|
+
|
70
|
+
response.result
|
71
|
+
=> "Added 2 invitations to QuestId:4567668"
|
72
|
+
|
73
|
+
|
74
|
+
# Add respondent data
|
75
|
+
response = api.add_respondents_data(
|
76
|
+
quest_info: {quest_id: 4567668, security_lock: 'm0pI8orKJp'},
|
77
|
+
respondents_data: {
|
78
|
+
respondent_data_header: {
|
79
|
+
respondent_data_header: [
|
80
|
+
{
|
81
|
+
title: 'Epost',
|
82
|
+
type: QuestBack::Api.respondent_data_header_type_for(:text),
|
83
|
+
is_email_field: true,
|
84
|
+
is_sms_field: false,
|
85
|
+
},
|
86
|
+
{
|
87
|
+
title: 'Navn',
|
88
|
+
type: QuestBack::Api.respondent_data_header_type_for(:text),
|
89
|
+
is_email_field: false,
|
90
|
+
is_sms_field: false,
|
91
|
+
},
|
92
|
+
{
|
93
|
+
title: 'Alder',
|
94
|
+
type: QuestBack::Api.respondent_data_header_type_for(:numeric),
|
95
|
+
is_email_field: false,
|
96
|
+
is_sms_field: false,
|
97
|
+
},
|
98
|
+
]
|
99
|
+
},
|
100
|
+
respondent_data: ['th@skalar.no;Thorbjorn;32'],
|
101
|
+
allow_duplicate: true,
|
102
|
+
add_as_invitee: true
|
103
|
+
}
|
104
|
+
)
|
105
|
+
|
106
|
+
response.result
|
107
|
+
=> "Added 1 respondent data to QuestId :4567668"
|
108
|
+
```
|
109
|
+
|
110
|
+
### Debug XML without making a request
|
111
|
+
|
112
|
+
If you ever need to see XML generated without sending the request it can be done by doing:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
QuestBack.debug!
|
116
|
+
QuestBack::Api.new.test_connection
|
117
|
+
|
118
|
+
DEBUG -- : HTTPI GET request to integration.questback.com (net_http)
|
119
|
+
INFO -- : !!!!!!!!!
|
120
|
+
INFO -- : !!! SOAP request hijacked by QuestBack::DebugObserver.
|
121
|
+
INFO -- : !!!!!!!!!
|
122
|
+
DEBUG -- :
|
123
|
+
|
124
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
125
|
+
<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:wsdl="https://integration.questback.com/2011/03" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:array="http://schemas.microsoft.com/2003/10/Serialization/Arrays" xmlns:enum="http://schemas.microsoft.com/2003/10/Serialization/Enums">
|
126
|
+
<env:Body>
|
127
|
+
<wsdl:TestConnection>
|
128
|
+
<wsdl:userInfo>
|
129
|
+
<wsdl:Username>inviso@skalar.no</wsdl:Username>
|
130
|
+
<wsdl:Password>xxxxx</wsdl:Password>
|
131
|
+
</wsdl:userInfo>
|
132
|
+
</wsdl:TestConnection>
|
133
|
+
</env:Body>
|
134
|
+
</env:Envelope>
|
135
|
+
|
136
|
+
QuestBack.remove_debug! # Activates real requests again
|
137
|
+
```
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
## Contributing
|
142
|
+
|
143
|
+
1. Fork it (https://github.com/Skalar/quest_back/fork)
|
144
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
145
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
146
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
147
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/config.example.yml
ADDED
@@ -0,0 +1,310 @@
|
|
1
|
+
module QuestBack
|
2
|
+
class Api
|
3
|
+
# This hash contains parts of request we can include in soap operation.
|
4
|
+
# For instance call(:some_action, attributes, include_defaults: [:paging_info]) will
|
5
|
+
# slice paging_info and include it in the request.
|
6
|
+
DEFAULTS = {
|
7
|
+
paging_info: {page_no: 0, page_size: 50},
|
8
|
+
quest_filter: '',
|
9
|
+
sendduplicate: false,
|
10
|
+
respondents_data: {
|
11
|
+
delimiter: ';',
|
12
|
+
order!: [:respondent_data_header, :respondent_data, :delimiter, :allow_duplicate, :add_as_invitee]
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
# The order of the elements in the SOAP body is important for the SOAP API.
|
17
|
+
# For operations with multiple arguments this hash gives savon the order of which
|
18
|
+
# it should .. well, order the elements.
|
19
|
+
ORDER = {
|
20
|
+
get_quests: [:user_info, :paging_info, :quest_filter],
|
21
|
+
add_email_invitees: [:user_info, :quest_info, :emails, :sendduplicate, :language_id],
|
22
|
+
add_respondents_data: [:user_info, :quest_info, :respondents_data, :language_id]
|
23
|
+
}
|
24
|
+
|
25
|
+
# In order to provide a simple response.result and response.results interface
|
26
|
+
# where the actual result we care about is returned we have to give knowledge to
|
27
|
+
# where this result is found. As it turns out, get_quests returns it's quests within
|
28
|
+
# quests/quest array, and at the same time get_quest_questions returns the questions
|
29
|
+
# within simply it's root result element. No nestings there.. So, it seems a bit randon
|
30
|
+
# and we need to have this configured. I though it would be put under quest_questions/quest_question,
|
31
|
+
# but no such luck.
|
32
|
+
RESULT_KEY_NESTINGS = {
|
33
|
+
test_connection: [],
|
34
|
+
get_quests: [:quests, :quest],
|
35
|
+
get_language_list: [:language],
|
36
|
+
add_email_invitees: [],
|
37
|
+
add_respondents_data: []
|
38
|
+
}
|
39
|
+
|
40
|
+
RESPONDENTS_HEADER_TYPE = {
|
41
|
+
numeric: 1,
|
42
|
+
text: 2
|
43
|
+
}
|
44
|
+
|
45
|
+
NAMESPACES = {
|
46
|
+
'xmlns:array' => 'http://schemas.microsoft.com/2003/10/Serialization/Arrays',
|
47
|
+
'xmlns:enum' => 'http://schemas.microsoft.com/2003/10/Serialization/Enums'
|
48
|
+
}
|
49
|
+
|
50
|
+
def self.respondent_data_header_type_for(type)
|
51
|
+
RESPONDENTS_HEADER_TYPE.fetch(type.to_sym) { fail ArgumentError, "#{type.to_s.inspect} is an unkown respondent data header type." }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Public: Creates a new API gateway object.
|
55
|
+
#
|
56
|
+
# Attributes
|
57
|
+
# config - A QuestBack::Configuration object. May be nil if
|
58
|
+
# QuestBack.default_configuration has been set.
|
59
|
+
def initialize(attributes = {})
|
60
|
+
attributes = ActiveSupport::HashWithIndifferentAccess.new attributes
|
61
|
+
|
62
|
+
@config = attributes[:config]
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
# Public: Make a test connection call to QuestBack
|
72
|
+
#
|
73
|
+
# Returns QuestBack::Response
|
74
|
+
def test_connection
|
75
|
+
call :test_connection
|
76
|
+
end
|
77
|
+
|
78
|
+
# Public: Get quests
|
79
|
+
#
|
80
|
+
# attributes - Attributes sent to QuestBack
|
81
|
+
#
|
82
|
+
# Example
|
83
|
+
#
|
84
|
+
# response = api.get_quests paging_info: {page_size: 2} # Limits result to two
|
85
|
+
# response.results
|
86
|
+
# => [result, result]
|
87
|
+
#
|
88
|
+
# Returns QuestBack::Response
|
89
|
+
def get_quests(attributes = {})
|
90
|
+
call :get_quests, attributes, include_defaults: [:paging_info, :quest_filter]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Public: Returns a list of languages from QuestBack.
|
94
|
+
#
|
95
|
+
#
|
96
|
+
# Returns QuestBack::Response
|
97
|
+
def get_language_list
|
98
|
+
call :get_language_list
|
99
|
+
end
|
100
|
+
|
101
|
+
# Public: Invites a set of emails to a quest.
|
102
|
+
#
|
103
|
+
# attributes - Attributes sent to QuestBack
|
104
|
+
#
|
105
|
+
# Example
|
106
|
+
#
|
107
|
+
# response = api.add_email_invitees(
|
108
|
+
# quest_info: {quest_id: 4567668, security_lock: 'm0pI8orKJp'},
|
109
|
+
# emails: ['inviso@skalar.no', 'th@skalar.no'],
|
110
|
+
# sendduplicate: true, # or false as default
|
111
|
+
# language_id: 123, # optional
|
112
|
+
# )
|
113
|
+
#
|
114
|
+
# Returns QuestBack::Response
|
115
|
+
def add_email_invitees(attributes = {})
|
116
|
+
call :add_email_invitees, attributes, include_defaults: [:sendduplicate]
|
117
|
+
end
|
118
|
+
|
119
|
+
# Public: Add respondent data to a quest - optionally send as invitee as well.
|
120
|
+
#
|
121
|
+
# attributes - Attributes sent to QuestBack
|
122
|
+
#
|
123
|
+
# QuestBack is doing a bit of CSV over XML here? As you need to serialize
|
124
|
+
# respondent_data as a string with a delimiter ala CSV. The order of the
|
125
|
+
# data must match the order of respondent_data_header. I guess simply using XML
|
126
|
+
# and named elements was too easy? :-)
|
127
|
+
#
|
128
|
+
# Example
|
129
|
+
#
|
130
|
+
# response = api.add_respondents_data(
|
131
|
+
# quest_info: {quest_id: 4567668, security_lock: 'm0pI8orKJp'},
|
132
|
+
# respondents_data: {
|
133
|
+
# respondent_data_header: {
|
134
|
+
# respondent_data_header: [
|
135
|
+
# {
|
136
|
+
# title: 'Epost',
|
137
|
+
# type: QuestBack::Api.respondent_data_header_type_for(:text),
|
138
|
+
# is_email_field: true,
|
139
|
+
# is_sms_field: false,
|
140
|
+
# },
|
141
|
+
# {
|
142
|
+
# title: 'Navn',
|
143
|
+
# type: QuestBack::Api.respondent_data_header_type_for(:text),
|
144
|
+
# is_email_field: false,
|
145
|
+
# is_sms_field: false,
|
146
|
+
# },
|
147
|
+
# {
|
148
|
+
# title: 'Alder',
|
149
|
+
# type: QuestBack::Api.respondent_data_header_type_for(:numeric),
|
150
|
+
# is_email_field: false,
|
151
|
+
# is_sms_field: false,
|
152
|
+
# },
|
153
|
+
# ]
|
154
|
+
# },
|
155
|
+
# respondent_data: ['th@skalar.no;Thorbjorn;32'], # According to QuestBack's doc you can only do one here
|
156
|
+
# allow_duplicate: true,
|
157
|
+
# add_as_invitee: true
|
158
|
+
# }
|
159
|
+
# )
|
160
|
+
#
|
161
|
+
# You may override respondent_data's delimiter in string too.
|
162
|
+
#
|
163
|
+
# Returns QuestBack::Response
|
164
|
+
def add_respondents_data(attributes = {})
|
165
|
+
call :add_respondents_data, attributes, include_defaults: [:respondents_data]
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
|
170
|
+
|
171
|
+
|
172
|
+
# Public: Savon client.
|
173
|
+
#
|
174
|
+
# Savon client all API method calls will go through.
|
175
|
+
def client
|
176
|
+
@client ||= begin
|
177
|
+
client_config = {
|
178
|
+
wsdl: config.wsdl,
|
179
|
+
namespace: config.soap_namespace,
|
180
|
+
log_level: config.log_level,
|
181
|
+
element_form_default: :qualified,
|
182
|
+
namespaces: NAMESPACES
|
183
|
+
}
|
184
|
+
|
185
|
+
client_config[:proxy] = config.http_proxy if config.http_proxy.present?
|
186
|
+
|
187
|
+
Savon::Client.new client_config
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
# Public: Configuration for the API.
|
193
|
+
#
|
194
|
+
# Returns a QuestBack::Configuration object
|
195
|
+
def config
|
196
|
+
@config || QuestBack.default_configuration || fail(QuestBack::Error, 'No configuration given or found on QuestBack.default_configuration.')
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
def call(operation_name, attributes = {}, options = {})
|
205
|
+
options[:operation_name] = operation_name
|
206
|
+
|
207
|
+
options_to_response = {
|
208
|
+
operation_name: options[:operation_name],
|
209
|
+
result_key_nestings: RESULT_KEY_NESTINGS.fetch(operation_name) { fail KeyError, "You must configure RESULT_KEY_NESTINGS for #{operation_name}" }
|
210
|
+
}
|
211
|
+
|
212
|
+
savon_response = client.call operation_name, build_hash_for_savon_call(attributes, options)
|
213
|
+
|
214
|
+
Response.new savon_response, options_to_response
|
215
|
+
end
|
216
|
+
|
217
|
+
|
218
|
+
# Private: Builds a hash for savon call - include user info and other defaults you ask it to
|
219
|
+
#
|
220
|
+
# attributes - A hash representing attributes the client sent to us which it expects us to send to QuestBack
|
221
|
+
# options - A hash where we can send in options:
|
222
|
+
# :include_defaults - Give an array with key names to slice from DEFAULTS and mix in with
|
223
|
+
# the rest of the attributes.
|
224
|
+
#
|
225
|
+
# Returns a merged hash for Savon client
|
226
|
+
def build_hash_for_savon_call(attributes = {}, options = {})
|
227
|
+
user_info = {user_info: {username: config.username, password: config.password}}
|
228
|
+
message = user_info.merge attributes
|
229
|
+
|
230
|
+
if default_keys = options[:include_defaults]
|
231
|
+
message = DEFAULTS.slice(*Array.wrap(default_keys)).deep_merge message
|
232
|
+
end
|
233
|
+
|
234
|
+
if order = ORDER[options[:operation_name]]
|
235
|
+
unkown_keys = attributes.keys - order
|
236
|
+
|
237
|
+
if unkown_keys.any?
|
238
|
+
fail ArgumentError, "Unkown attributes given to #{options[:operation_name]}: #{unkown_keys.join(', ')}. Attributes' order needs to be configured in #{self.class.name}::ORDER."
|
239
|
+
end
|
240
|
+
|
241
|
+
message[:order!] = order & message.keys
|
242
|
+
end
|
243
|
+
|
244
|
+
{
|
245
|
+
message: transform_hash_for_quest_back(message)
|
246
|
+
}
|
247
|
+
end
|
248
|
+
|
249
|
+
|
250
|
+
# Private: Transforms given hash as how Savon needs it to build the correct SOAP body.
|
251
|
+
#
|
252
|
+
# Since QuestBack's API needs to have elements like this:
|
253
|
+
#
|
254
|
+
# <wsdl:TestConnection>
|
255
|
+
# <wsdl:userInfo>
|
256
|
+
# <wsdl:Username>
|
257
|
+
# ...
|
258
|
+
#
|
259
|
+
# We cannot simply use Savon's convert_request_keys_to config, as it translate all keys.
|
260
|
+
# We need some keys camelcased (keys within nested hashes) and some lower_camelcased (keys in the outer most hash).
|
261
|
+
#
|
262
|
+
# Thus we map our inner attributes, for instance for userInfo to camelcase and keeps them
|
263
|
+
# as strings so Savon will not manipulate them.
|
264
|
+
#
|
265
|
+
# I guess this helper method here is kinda not optimal, and we may have a simple class / struct
|
266
|
+
# which can do this job for us, so the api class does not have multiple responsibilites. Oh well,
|
267
|
+
# works for now.
|
268
|
+
def transform_hash_for_quest_back(hash, transform_keys = false)
|
269
|
+
Hash[
|
270
|
+
hash.map do |key, value|
|
271
|
+
if key == :order!
|
272
|
+
# Key was :order! - it has special meaning: The symbols within it's array are used to
|
273
|
+
# dictate order of elements. If transform_keys is false we are on "root keys". These are
|
274
|
+
# keept as symbols and Savon does it's magic and we'll do nothing. If it is true it means that keys
|
275
|
+
# on this level is put to camelcase and the values in the :order! array must match this.
|
276
|
+
if transform_keys
|
277
|
+
value = value.map { |v| v.to_s.camelcase }
|
278
|
+
end
|
279
|
+
else
|
280
|
+
key = transform_keys ? key.to_s.camelcase : key
|
281
|
+
|
282
|
+
# Oh my god this is quick, dirty and mega hackish!
|
283
|
+
# Type element in the RespondentDataHeader must be in namespace enum.
|
284
|
+
key = "enum:Type" if key == "Type"
|
285
|
+
|
286
|
+
# In some cases we would like to transform values as well as the key
|
287
|
+
value = case value
|
288
|
+
when Hash
|
289
|
+
# Keep on transforming recursively..
|
290
|
+
transform_hash_for_quest_back value, true
|
291
|
+
when Array
|
292
|
+
if value.all? { |v| v.is_a? String }
|
293
|
+
# Put it in a structure QuestBack likes..
|
294
|
+
{'array:string' => value}
|
295
|
+
elsif value.all? { |v| v.is_a? Hash }
|
296
|
+
# Keep on transforming recursively..
|
297
|
+
value.map { |hash| transform_hash_for_quest_back(hash, true) }
|
298
|
+
end
|
299
|
+
else
|
300
|
+
# We don't know anything better - just let value fall through
|
301
|
+
value
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
[key, value]
|
306
|
+
end
|
307
|
+
]
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module QuestBack
|
2
|
+
class Configuration
|
3
|
+
API_DEFAULTS = {
|
4
|
+
wsdl: 'https://integration.questback.com/integration.svc?wsdl',
|
5
|
+
soap_namespace: 'https://integration.questback.com/2011/03',
|
6
|
+
log_level: :debug
|
7
|
+
}
|
8
|
+
|
9
|
+
attr_accessor :http_proxy, :wsdl, :soap_namespace, :log_level, :username, :password
|
10
|
+
|
11
|
+
def initialize(attributes = {})
|
12
|
+
assign API_DEFAULTS
|
13
|
+
assign attributes
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(name, value)
|
17
|
+
public_send "#{name}=", value
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](name)
|
21
|
+
public_send name
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def assign(attributes)
|
29
|
+
attributes.each_pair do |name, value|
|
30
|
+
self[name] = value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module QuestBack
|
2
|
+
# Public: Simple debug observer.
|
3
|
+
#
|
4
|
+
# Hijacks operations and pretty prints XML which could have been sent.
|
5
|
+
class DebugObserver
|
6
|
+
def notify(operation_name, builder, globals, locals)
|
7
|
+
logger = globals[:logger]
|
8
|
+
|
9
|
+
logger.info "!!!!!!!!!"
|
10
|
+
logger.info "!!! SOAP request hijacked by #{self.class.name}."
|
11
|
+
logger.info "!!!!!!!!!"
|
12
|
+
|
13
|
+
logger.debug "\n\n" + Nokogiri.XML(builder.to_s).to_xml + "\n"
|
14
|
+
|
15
|
+
HTTPI::Response.new(200, {}, '')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module QuestBack
|
2
|
+
# Public: Simple proxy object decorating Savon's response.
|
3
|
+
#
|
4
|
+
# Main motication for class is to have #result and #results as a common
|
5
|
+
# interface to get to respons' result(s), with all outer response nesting
|
6
|
+
# removed.
|
7
|
+
#
|
8
|
+
# At the moment you are still getting back simple hashes from both methods.
|
9
|
+
class Response
|
10
|
+
attr_reader :savon_response, :operation_name, :result_key_nestings
|
11
|
+
|
12
|
+
delegate *Savon::Response.instance_methods(false), to: :savon_response
|
13
|
+
|
14
|
+
def initialize(savon_response, options = {})
|
15
|
+
@savon_response = savon_response
|
16
|
+
@operation_name = options[:operation_name] or fail ArgumentError.new('Missing operation_name')
|
17
|
+
@result_key_nestings = options[:result_key_nestings] or fail ArgumentError.new('Missing result key nestings')
|
18
|
+
end
|
19
|
+
|
20
|
+
def result
|
21
|
+
extract_result.tap do |result|
|
22
|
+
fail QuestBack::Error::MultipleResultsFound if result.is_a? Array
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def results
|
27
|
+
Array.wrap extract_result
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def extract_result
|
36
|
+
result_container = savon_response.body["#{operation_name}_response".to_sym]["#{operation_name}_result".to_sym]
|
37
|
+
|
38
|
+
result = result_key_nestings.inject(result_container) do |result, key|
|
39
|
+
result.fetch key do
|
40
|
+
fail KeyError, "Expected #{result.inspect} to contain #{key_in_body}. Respons' result_key_nestings is wrong, or unexpected result from QuestBack."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/quest_back.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require "quest_back/version"
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "active_support/all"
|
5
|
+
require "savon"
|
6
|
+
|
7
|
+
|
8
|
+
module QuestBack
|
9
|
+
extend ActiveSupport::Autoload
|
10
|
+
|
11
|
+
autoload :Configuration
|
12
|
+
autoload :Error
|
13
|
+
autoload :Response
|
14
|
+
autoload :Api
|
15
|
+
autoload :DebugObserver
|
16
|
+
|
17
|
+
|
18
|
+
# Public: Default configuration is read from here.
|
19
|
+
#
|
20
|
+
# Returns a Configuration object
|
21
|
+
mattr_accessor :default_configuration
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
# :nodoc:
|
28
|
+
#
|
29
|
+
# Read config from file, just makes it easy for me to spin up
|
30
|
+
# a console, read API credentials and get going
|
31
|
+
def self.conf!
|
32
|
+
read_default_configuration_from_file 'config.yml'
|
33
|
+
end
|
34
|
+
|
35
|
+
# :nodoc:
|
36
|
+
#
|
37
|
+
# Injects a debug observer which hijacks requests so we don't send anything.
|
38
|
+
# We'll pretty print the SOAP body for inspection.
|
39
|
+
def self.debug!
|
40
|
+
remove_debug!
|
41
|
+
Savon.observers << DebugObserver.new
|
42
|
+
end
|
43
|
+
|
44
|
+
# :nodoc:
|
45
|
+
#
|
46
|
+
# Removes any debug observers.
|
47
|
+
def self.remove_debug!
|
48
|
+
Savon.observers.delete_if { |observer| observer.is_a? DebugObserver }
|
49
|
+
end
|
50
|
+
|
51
|
+
# :nodoc:
|
52
|
+
def self.read_default_configuration_from_file(pathname)
|
53
|
+
path = Pathname.new pathname
|
54
|
+
path = Pathname.new [Dir.pwd, pathname].join('/') unless path.absolute?
|
55
|
+
self.default_configuration = Configuration.new YAML.load_file(path)
|
56
|
+
end
|
57
|
+
end
|