curbala 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +4 -0
- data/Gemfile +53 -0
- data/LICENSE +21 -0
- data/README.rdoc +410 -0
- data/Rakefile +18 -0
- data/init.rb +1 -0
- data/lib/curbala.rb +2 -0
- data/lib/curbala/action.rb +123 -0
- data/lib/curbala/service.rb +33 -0
- data/lib/generators/curbala/action/USAGE +5 -0
- data/lib/generators/curbala/action/action_generator.rb +47 -0
- data/lib/generators/curbala/action/template/action.rb +62 -0
- data/lib/generators/curbala/action/template/action_spec.rb +75 -0
- data/lib/generators/curbala/action/template/config.yml +26 -0
- data/lib/generators/curbala/action/template/service.rb +24 -0
- data/spec/curbala/action_spec.rb +161 -0
- data/spec/curbala/service_spec.rb +76 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +9 -0
- metadata +118 -0
data/CHANGELOG.rdoc
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# source 'https://rubygems.org'
|
2
|
+
#
|
3
|
+
# gem 'rails', '3.2.3'
|
4
|
+
#
|
5
|
+
# # Bundle edge Rails instead:
|
6
|
+
# # gem 'rails', :git => 'git://github.com/rails/rails.git'
|
7
|
+
#
|
8
|
+
# gem 'mysql'
|
9
|
+
#
|
10
|
+
# gem 'json'
|
11
|
+
#
|
12
|
+
# # Gems used only for assets and not required
|
13
|
+
# # in production environments by default.
|
14
|
+
# group :assets do
|
15
|
+
# gem 'sass-rails', '~> 3.2.3'
|
16
|
+
# gem 'coffee-rails', '~> 3.2.1'
|
17
|
+
#
|
18
|
+
# # See https://github.com/sstephenson/execjs#readme for more supported runtimes
|
19
|
+
# # gem 'therubyracer', :platform => :ruby
|
20
|
+
#
|
21
|
+
# gem 'uglifier', '>= 1.0.3'
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# gem 'jquery-rails'
|
25
|
+
#
|
26
|
+
# # To use ActiveModel has_secure_password
|
27
|
+
# # gem 'bcrypt-ruby', '~> 3.0.0'
|
28
|
+
#
|
29
|
+
# # To use Jbuilder templates for JSON
|
30
|
+
# # gem 'jbuilder'
|
31
|
+
#
|
32
|
+
# # Use unicorn as the app server
|
33
|
+
# # gem 'unicorn'
|
34
|
+
#
|
35
|
+
# # Deploy with Capistrano
|
36
|
+
# # gem 'capistrano'
|
37
|
+
#
|
38
|
+
# # To use debugger
|
39
|
+
# # gem 'ruby-debug'
|
40
|
+
#
|
41
|
+
# gem 'authlogic', '>= 3.1.0'
|
42
|
+
|
43
|
+
source :rubygems
|
44
|
+
|
45
|
+
# gem "sqlite3"
|
46
|
+
# gem "activerecord", '~> 3.0.9', :require => "active_record"
|
47
|
+
# gem "with_model", "~> 0.2.5"
|
48
|
+
# gem "meta_where"
|
49
|
+
|
50
|
+
gemspec
|
51
|
+
|
52
|
+
gem 'rspec', "~> 2.10.0"
|
53
|
+
gem 'curb', '~> 0.8.1'
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2012 Frederick Fix
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
data/README.rdoc
ADDED
@@ -0,0 +1,410 @@
|
|
1
|
+
= Curbala
|
2
|
+
|
3
|
+
Wiki[https://github.com/rickfix/curbala/wiki] | RDocs[http://rdoc.info/projects/rickfix/curbala]
|
4
|
+
|
5
|
+
Curbala is a curb[http://rubygems.org/gems/curb/] wrapper that acts as a client for externally-hosted services.
|
6
|
+
|
7
|
+
In the system which Curbala was extracted from, the original curbala-esque implementation addressed some issues/requirements:
|
8
|
+
|
9
|
+
1. environment specific configuration of base service URLs
|
10
|
+
2. DRY'd references to base service URLs
|
11
|
+
3. different http/https requirements in different environments
|
12
|
+
4. a means to simulate service calls
|
13
|
+
+ decoupled rails development from api development when interfaces were defined but service was not available yet or was broke.
|
14
|
+
+ some of our services were not available in development mode
|
15
|
+
+ did not want to hit external services in specs
|
16
|
+
5. our internal services developers used curl to test their work. wrapping curb provided good common ground.
|
17
|
+
6. a log trail of each service call was desirable
|
18
|
+
+ the application data which the action request was constructed.
|
19
|
+
+ the url that was invoked
|
20
|
+
+ the payload that was sent (post, put)
|
21
|
+
+ the http status of the request.
|
22
|
+
+ the raw and unpacked response
|
23
|
+
+ our non-production systems where doing logging of each database read and write.
|
24
|
+
makes at least as much sense to log similar stuff for each service call.
|
25
|
+
7. some service calls were inline in controllers and exhibited 'long line' and 'long arg list' code smells
|
26
|
+
|
27
|
+
|
28
|
+
Curbala is implemented as a framework pattern which requires extending classes to implement two methods:
|
29
|
+
1. action_url_segment()
|
30
|
+
2. invoke_action()
|
31
|
+
|
32
|
+
Extending classes can also implement/override base class implementations of the following methods:
|
33
|
+
1. unpack_response() : perform xml/json/... response extraction/conversion
|
34
|
+
2. success_message()
|
35
|
+
3. fail_message()
|
36
|
+
|
37
|
+
|
38
|
+
== Installation
|
39
|
+
|
40
|
+
1. In <b>Rails 3</b>, add this to your Gemfile and run the +bundle+ command.
|
41
|
+
|
42
|
+
gem "curbala"
|
43
|
+
gem "curb" # currently tested against 0.8.1 : need to rectify when your app already uses curb...
|
44
|
+
|
45
|
+
2. bundle exec gem install
|
46
|
+
|
47
|
+
should pull in curbala (and possibly curb)
|
48
|
+
|
49
|
+
== Getting Started
|
50
|
+
|
51
|
+
Try using the curbala action generator in your app:
|
52
|
+
|
53
|
+
This example sets uses the publicly accessible Acromine service as a rudimentary intro to curbala.
|
54
|
+
The Acromine service is accessible at http://www.nactem.ac.uk/software/acromine/dictionary.py?sf=QUERY
|
55
|
+
(where QUERY is the acronym to query on).
|
56
|
+
|
57
|
+
Run the curbala action generator: it will ask you 4 questions:
|
58
|
+
|
59
|
+
$ rails g curbala:action
|
60
|
+
|
61
|
+
a
|
62
|
+
Enter path to directory where service/action should be installed [app/models] :
|
63
|
+
What is the service name for the new action? cromine
|
64
|
+
create config/acromine.yml
|
65
|
+
create app/models/acromine/service.rb
|
66
|
+
|
67
|
+
What is the new action name? get
|
68
|
+
create app/models/acromine/get.rb
|
69
|
+
|
70
|
+
Generate spec/models/acromine/get_spec.rb? [Yn] Y
|
71
|
+
create spec/models/acromine/get_spec.rb
|
72
|
+
gsub config/acromine.yml
|
73
|
+
gsub config/acromine.yml
|
74
|
+
gsub config/acromine.yml
|
75
|
+
gsub app/models/acromine/service.rb
|
76
|
+
gsub app/models/acromine/service.rb
|
77
|
+
gsub app/models/acromine/service.rb
|
78
|
+
gsub app/models/acromine/get.rb
|
79
|
+
gsub app/models/acromine/get.rb
|
80
|
+
gsub app/models/acromine/get.rb
|
81
|
+
gsub spec/models/acromine/get_spec.rb
|
82
|
+
gsub spec/models/acromine/get_spec.rb
|
83
|
+
gsub spec/models/acromine/get_spec.rb
|
84
|
+
|
85
|
+
What just happened?
|
86
|
+
|
87
|
+
1. The generator asked :
|
88
|
+
|
89
|
+
Enter path to directory where service/action should be installed [app/models] :
|
90
|
+
|
91
|
+
* default (taken in this example) is app/models
|
92
|
+
|
93
|
+
+ you could specify something like app/clients or lib or lib/services/clients, whatever directory adheres to your apps organizational sensibilities
|
94
|
+
|
95
|
+
|
96
|
+
2.1 The generator asked for the name of the service, and *acromine* was entered:
|
97
|
+
|
98
|
+
What is the service name for the new action? acromine
|
99
|
+
|
100
|
+
|
101
|
+
2.2 Two files were created:
|
102
|
+
|
103
|
+
config/acromine.yml
|
104
|
+
|
105
|
+
app/models/acromine/service.rb
|
106
|
+
|
107
|
+
|
108
|
+
3.1 The generator asked for the action name and *get* was entered:
|
109
|
+
|
110
|
+
What is the new action name? get
|
111
|
+
|
112
|
+
3.2 The action class for get was created:
|
113
|
+
|
114
|
+
app/models/acromine/get.rb
|
115
|
+
|
116
|
+
|
117
|
+
4.1 The generator asked if a spec should be generated and *Y* (to indicate yes) was entered:
|
118
|
+
|
119
|
+
Generate spec/models/acromine/get_spec.rb? [Yn] Y
|
120
|
+
|
121
|
+
4.2 A spec was generated:
|
122
|
+
|
123
|
+
spec/models/acromine/get_spec.rb
|
124
|
+
|
125
|
+
Hopefully your app is already using rspec.
|
126
|
+
|
127
|
+
The generated spec is designed to help red/green your way toward gluing your config, service and action components together.
|
128
|
+
|
129
|
+
|
130
|
+
Run specs and you should see something like:
|
131
|
+
|
132
|
+
$ bundle exec rake spec
|
133
|
+
|
134
|
+
Failures:
|
135
|
+
|
136
|
+
1) Acromine::Get invoke_action and http response code is 200 should indicate success
|
137
|
+
Failure/Error: @curbala_instance.url.should == @expected_url
|
138
|
+
expected: "http://base service url/service url segment/action url segment"
|
139
|
+
got: "http://base service url/service url segment/construct action segment of Acromine Get url in action_url_segment() method at /path to your/app/models/acromine/get.rb:9" (using ==)
|
140
|
+
# ./spec/models/acromine/get_spec.rb:66:in `verify_curbala_action'
|
141
|
+
# ./spec/models/acromine/get_spec.rb:49
|
142
|
+
|
143
|
+
|
144
|
+
Notice the instructive portion of the 'got:' message:
|
145
|
+
|
146
|
+
*construct action segment of Acromine Get url in action_url_segment() method at /path to your/app/models/acromine/get.rb:9*
|
147
|
+
|
148
|
+
|
149
|
+
This is where you must decide how you want to slice up the service url among config, service and action.
|
150
|
+
|
151
|
+
The acromine service url is http://www.nactem.ac.uk/software/acromine/dictionary.py?sf=QUERY
|
152
|
+
|
153
|
+
There are a number of ways you can slice this one.
|
154
|
+
1. config/acromine.yml: http://www.nactem.ac.uk/software/acromine/
|
155
|
+
app/models/acromine/service.rb:service_url_segment() : "dictionary.py"
|
156
|
+
app/models/acromine/get.rb:action_url_segment() : "?sf=#{@args_hash['sf']}"
|
157
|
+
|
158
|
+
2. config/acromine.yml: http://www.nactem.ac.uk/software/acromine/dictionary.py?sf=
|
159
|
+
app/models/acromine/service.rb:service_url_segment() : ""
|
160
|
+
app/models/acromine/get.rb:action_url_segment() : @args_hash['sf']
|
161
|
+
|
162
|
+
3. config/acromine.yml: http://www.nactem.ac.uk/
|
163
|
+
app/models/acromine/service.rb:service_url_segment() : "software/acromine/"
|
164
|
+
app/models/acromine/get.rb:action_url_segment() : "dictionary.py?sf=#{@args_hash['sf']}"
|
165
|
+
|
166
|
+
|
167
|
+
Option 1. most appealed to my aesthetic sense when I was playing with this example,
|
168
|
+
so I edited the 3 files/methods as indicated.
|
169
|
+
|
170
|
+
|
171
|
+
Rerun specs and should see something like:
|
172
|
+
|
173
|
+
Failures:
|
174
|
+
|
175
|
+
1) Acromine::Get invoke_action and http response code is 200 should indicate success
|
176
|
+
Failure/Error: @curbala_instance.url.should == @expected_url
|
177
|
+
expected: "http://base service url/service url segment/action url segment"
|
178
|
+
got: "http://base service url/service url segment/?sf=" (using ==)
|
179
|
+
# ./spec/models/acromine/get_spec.rb:66:in `verify_curbala_action'
|
180
|
+
# ./spec/models/acromine/get_spec.rb:49
|
181
|
+
|
182
|
+
|
183
|
+
The 'got:' value shows that the Acromine::Get.action_url_segment() seems to be doing its thing.
|
184
|
+
|
185
|
+
Time to update expectations in the spec
|
186
|
+
Insert the following two lines at spec/models/acromine/get_spec.rb:49
|
187
|
+
|
188
|
+
@args_hash['sf'] = 'HTTP'
|
189
|
+
@expected_url = "http://base service url/service url segment/?sf=HTTP"
|
190
|
+
|
191
|
+
|
192
|
+
Rerun specs and should see something like:
|
193
|
+
|
194
|
+
Failures:
|
195
|
+
|
196
|
+
1) Acromine::Get invoke_action and http response code is 200 should indicate success
|
197
|
+
Failure/Error: @curbala_instance.message.should == @expected_message
|
198
|
+
expected: "Successful (200)"
|
199
|
+
got: "Service Not Available: undefined method `implement invoke_action() method in /path to your/app/models/acromine/get.rb:12' for #<Acromine::Get:0x10e5400b0>" (using ==)
|
200
|
+
# ./spec/models/acromine/get_spec.rb:69:in `verify_curbala_action'
|
201
|
+
# ./spec/models/acromine/get_spec.rb:51
|
202
|
+
|
203
|
+
The 'got:' message is leading us to the next step to glue the Get action:
|
204
|
+
'implement invoke_action() method in /path to your/app/models/acromine/get.rb:12'
|
205
|
+
|
206
|
+
The invoke_action is where you tinker with, and invoke an http action (get/put/post/delete) on, the Curl::Easy (from the curb gem) instance in the @curl class variable.
|
207
|
+
|
208
|
+
For the acromine example, we just have to invoke http_get,
|
209
|
+
so change the guts of app/models/acromine/get.rb:invoke_action() to be:
|
210
|
+
|
211
|
+
def invoke_action
|
212
|
+
curl.http_get
|
213
|
+
end
|
214
|
+
|
215
|
+
and adjust spec so that the http_get is expected by inserting the following prior at line 51:
|
216
|
+
|
217
|
+
@mocked_curl.should_receive(:http_get)
|
218
|
+
|
219
|
+
|
220
|
+
Rerun specs and they should pass.
|
221
|
+
|
222
|
+
|
223
|
+
In a browser, hit the following url:
|
224
|
+
http://www.nactem.ac.uk/software/acromine/dictionary.py?sf=HTTP
|
225
|
+
|
226
|
+
|
227
|
+
The response is JSON.
|
228
|
+
Curbala lets you massage/translate the raw string response however is appropriate
|
229
|
+
to get it in a format that is palatable for invoking models, controllers and views
|
230
|
+
by letting you override the Curbala::Action.unpack_response() base class implementation.
|
231
|
+
Curbala provides two methods for helping unpack repsonses: hash_from_xml_response() and hash_from_json_response().
|
232
|
+
|
233
|
+
|
234
|
+
Implement the following in app/models/acromine/get.rb:
|
235
|
+
|
236
|
+
def unpack_response
|
237
|
+
hash_from_json_response
|
238
|
+
@response = (@response[0]['lfs'] rescue 'no results') if @success == true
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
and adjust spec by inserting the following two lines at line spec/models/acromine/get_spec.rb:52
|
243
|
+
|
244
|
+
@mocked_response = '[{"sf": "HTTP", "lfs": [{"lf": "hypertext transfer protocol", "freq": 6, "since": 1995, "vars": [{"lf": "Hypertext Transfer Protocol", "freq": 3, "since": 1996}, {"lf": "hypertext transfer protocol", "freq": 3, "since": 1995}]}]}]'
|
245
|
+
@expected_response_data = [{"freq"=>6, "vars"=>[{"freq"=>3, "lf"=>"Hypertext Transfer Protocol", "since"=>1996}, {"freq"=>3, "lf"=>"hypertext transfer protocol", "since"=>1995}], "lf"=>"hypertext transfer protocol", "since"=>1995}]
|
246
|
+
|
247
|
+
|
248
|
+
Rerun specs and they should pass.
|
249
|
+
|
250
|
+
|
251
|
+
Oh goody, now we can try this in console (copy and paste the code snippet):
|
252
|
+
|
253
|
+
$ rails c
|
254
|
+
|
255
|
+
>> def acro(acronym)
|
256
|
+
args = {'sf' => acronym}
|
257
|
+
get = Acromine::Service.invoke(:get, args)
|
258
|
+
get.success && get.response.kind_of?(Array) ? get.response.collect{|h| h['lf']} : []
|
259
|
+
end
|
260
|
+
=> nil
|
261
|
+
>> # invoke the acromine service:
|
262
|
+
>? acro 'ABC'
|
263
|
+
|
264
|
+
*** you should see something like:
|
265
|
+
=> ["ATP-binding cassette", "avidin-biotin-peroxidase complex", "aneurysmal bone cyst", "abacavir", "advanced breast cancer", "antibody binding capacity", "Aberrant Behavior Checklist", "activity-based costing", "Activities-specific Balance Confidence", "argon beam coagulator", "aspiration biopsy cytology", "active breathing control", "activated B-cell-like", "absolute blast count", "adenoid basal carcinoma", "approximate Bayesian computation", "antibodies bound per cell", "alveolar bone crest", "accelerated blood clearance", "Alternative Birthing Center", "artificial beta cell", "American Biophysics Corporation", "adenine nucleotide binding cassette", "Movement Assessment Battery for Children", "Alcoholic Beverage Control", "The area between curves"]
|
266
|
+
|
267
|
+
>> acro 'A'
|
268
|
+
=> []
|
269
|
+
|
270
|
+
|
271
|
+
The acromine service is up and running and ready to be integrated into your models, controllers and views.
|
272
|
+
|
273
|
+
|
274
|
+
|
275
|
+
== Final version of files for acromine example
|
276
|
+
|
277
|
+
1. config/acromine.yml
|
278
|
+
|
279
|
+
|
280
|
+
url: http://www.nactem.ac.uk/software/acromine/
|
281
|
+
|
282
|
+
test:
|
283
|
+
simulate: true
|
284
|
+
|
285
|
+
|
286
|
+
2. app/models/acromine/service.rb
|
287
|
+
|
288
|
+
|
289
|
+
class Acromine::Service < Curbala::Service
|
290
|
+
|
291
|
+
def self.service_url_segment(associated_model)
|
292
|
+
"dictionary.py"
|
293
|
+
end
|
294
|
+
|
295
|
+
def self.service_qualifier
|
296
|
+
'Acromine'
|
297
|
+
end
|
298
|
+
|
299
|
+
def self.config_file
|
300
|
+
'acromine.yml'
|
301
|
+
end
|
302
|
+
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
3. app/models/acromine/get.rb
|
307
|
+
|
308
|
+
|
309
|
+
class Acromine::Get < Curbala::Action
|
310
|
+
|
311
|
+
def action_url_segment
|
312
|
+
"?sf=#{@args_hash['sf']}"
|
313
|
+
end
|
314
|
+
|
315
|
+
def invoke_action
|
316
|
+
curl.http_get
|
317
|
+
end
|
318
|
+
|
319
|
+
def unpack_response
|
320
|
+
hash_from_json_response
|
321
|
+
@response = (@response[0]['lfs'] rescue 'no results') if @success == true
|
322
|
+
end
|
323
|
+
|
324
|
+
end
|
325
|
+
|
326
|
+
|
327
|
+
4. spec/models/acromine/get_spec.rb
|
328
|
+
|
329
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
330
|
+
|
331
|
+
describe 'Acromine::Get' do
|
332
|
+
before(:each) do
|
333
|
+
@config = {'simulate' => true, 'url' => 'http://base service url/'}
|
334
|
+
(@logger = mock).should_receive(:debug).any_number_of_times
|
335
|
+
end
|
336
|
+
|
337
|
+
it "should source action_url_segment, success_message and simulated response from Acromine::Get implementation when simulated" do
|
338
|
+
Curl::Easy.should_receive(:new).never # simulated : invoke_action() is not invoked.
|
339
|
+
@curbala_action_instance = Acromine::Get.new('service url segment/', @config, {}, @logger)
|
340
|
+
@curbala_action_instance.url.should == "http://base service url/service url segment/#{@curbala_action_instance.action_url_segment}"
|
341
|
+
@curbala_action_instance.success.should be_true
|
342
|
+
@curbala_action_instance.http_status.should == 200
|
343
|
+
@curbala_action_instance.message.should == "Successful (200)"
|
344
|
+
@curbala_action_instance.response.should == "simulated response"
|
345
|
+
end
|
346
|
+
|
347
|
+
describe 'invoke_action' do
|
348
|
+
before(:each) do
|
349
|
+
@args_hash = {}
|
350
|
+
@config['simulate'] = false
|
351
|
+
@expected_url = "http://base service url/service url segment/dictionary.py?sf=HTTP"
|
352
|
+
@mocked_curl = mock # (:body_str => @mocked_response)
|
353
|
+
end
|
354
|
+
|
355
|
+
describe "and http response code is 200" do
|
356
|
+
|
357
|
+
it "should indicate success" do
|
358
|
+
@args_hash['sf'] = 'HTTP'
|
359
|
+
@mocked_response = '[{"sf": "HTTP", "lfs": [{"lf": "hypertext transfer protocol", "freq": 6, "since": 1995, "vars": [{"lf": "Hypertext Transfer Protocol", "freq": 3, "since": 1996}, {"lf": "hypertext transfer protocol", "freq": 3, "since": 1995}]}]}]'
|
360
|
+
@expected_response_data = [{"freq"=>6, "vars"=>[{"freq"=>3, "lf"=>"Hypertext Transfer Protocol", "since"=>1996}, {"freq"=>3, "lf"=>"hypertext transfer protocol", "since"=>1995}], "lf"=>"hypertext transfer protocol", "since"=>1995}]
|
361
|
+
@mocked_curl.should_receive(:http_get)
|
362
|
+
@expected_message = 'Successful (200)'
|
363
|
+
verify_curbala_action(Acromine::Get, 200, be_true)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
def verify_curbala_action(class_under_test, forced_http_status, be_expected_condition)
|
368
|
+
@mocked_curl.should_receive(:body_str).any_number_of_times.and_return(@mocked_response)
|
369
|
+
Curl::Easy.should_receive(:new).with(@expected_url).and_return(@mocked_curl)
|
370
|
+
@mocked_curl.should_receive(:timeout=).with(10)
|
371
|
+
@mocked_curl.should_receive(:response_code).any_number_of_times.and_return(forced_http_status)
|
372
|
+
@curbala_instance = class_under_test.new('service url segment/', @config, @args_hash, @logger)
|
373
|
+
@curbala_instance.url.should == @expected_url
|
374
|
+
@curbala_instance.message.should == @expected_message
|
375
|
+
@curbala_instance.success.should be_expected_condition
|
376
|
+
@curbala_instance.response.should == @expected_response_data
|
377
|
+
@curbala_instance.http_status.should == forced_http_status
|
378
|
+
end
|
379
|
+
|
380
|
+
end
|
381
|
+
|
382
|
+
end
|
383
|
+
|
384
|
+
|
385
|
+
|
386
|
+
== Wiki Docs
|
387
|
+
|
388
|
+
* {Home}[https://github.com/rickfix/curbala/wiki]
|
389
|
+
|
390
|
+
|
391
|
+
== Project Status
|
392
|
+
|
393
|
+
Active.
|
394
|
+
|
395
|
+
Extracted from non-gem implementation of in-production {ProfitSteams}[http://profitstreams.com] system.
|
396
|
+
|
397
|
+
|
398
|
+
== Questions or Problems?
|
399
|
+
|
400
|
+
If you have any issues with Curbala which you cannot find the solution to in the documentation[https://github.com/rickfix/curbala/wiki],
|
401
|
+
please add an {issue on GitHub}[https://github.com/rickfix/curbala/issues]
|
402
|
+
or fork the project and send a pull request.
|
403
|
+
|
404
|
+
If I have time, I'll try to help.
|
405
|
+
|
406
|
+
|
407
|
+
== Thanks
|
408
|
+
|
409
|
+
Thanks to Eric Rapp for permission to extract this gem
|
410
|
+
and for giving me plenty of space and time to tinker and tune.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc "Run RSpec"
|
6
|
+
RSpec::Core::RakeTask.new do |t|
|
7
|
+
t.verbose = false
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Run specs for all adapters"
|
11
|
+
task :spec_all do
|
12
|
+
%w[active_record].each do |model_adapter|
|
13
|
+
puts "MODEL_ADAPTER = #{model_adapter}"
|
14
|
+
system "rake spec MODEL_ADAPTER=#{model_adapter}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
task :default => :spec
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'curbala'
|
data/lib/curbala.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
# require 'rexml/document' # TODO : needed?
|
3
|
+
|
4
|
+
module Curbala
|
5
|
+
class Action
|
6
|
+
attr_accessor :args_hash, :config, :curl, :exception, :http_status, :message, :payload, :raw_response_string, :response, :success, :url
|
7
|
+
|
8
|
+
def initialize(service_url_segment, input_config, input_args_hash, logger, simulated_status=200, simulated_response="simulated response")
|
9
|
+
begin
|
10
|
+
|
11
|
+
@payload, @args_hash, @config = '', input_args_hash, input_config
|
12
|
+
@url = "#{@config['url']}#{service_url_segment}#{action_url_segment}"
|
13
|
+
|
14
|
+
@config['simulate'] == true ?
|
15
|
+
inject_status_and_response(simulated_status, simulated_response) :
|
16
|
+
invoke_curl_action
|
17
|
+
|
18
|
+
rescue Exception => @exception
|
19
|
+
|
20
|
+
@success = false
|
21
|
+
@message = "Service Not Available: #{@exception.message}" # : BACKTRACE: #{@exception.backtrace[0..500]}"
|
22
|
+
logger.debug "Exception occurred: #{@exception.message}: BACKTRACE: #{@exception.backtrace}"
|
23
|
+
@raw_response_string ||= 'exception occurred in action processing'
|
24
|
+
@response ||= @raw_response_string
|
25
|
+
@http_status ||= -1
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
self
|
30
|
+
|
31
|
+
ensure
|
32
|
+
|
33
|
+
logger.debug ''
|
34
|
+
logger.debug("#{self.class.name}: #{url}")
|
35
|
+
logger.debug("config: #{config.inspect}")
|
36
|
+
logger.debug("args_hash: #{args_hash.inspect}")
|
37
|
+
logger.debug("payload: #{payload.inspect}") unless payload.nil? || payload.empty?
|
38
|
+
logger.debug("http status: #{http_status}, success: #{success}, message: #{message}")
|
39
|
+
logger.debug("raw response string:")
|
40
|
+
logger.debug(raw_response_string.strip)
|
41
|
+
logger.debug("unpacked response: #{response.class.name}")
|
42
|
+
logger.debug(response.inspect)
|
43
|
+
logger.debug ''
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
# any url utilities (for extending class implementations of action_url_segment()) ?
|
48
|
+
|
49
|
+
# request utilities:
|
50
|
+
def xml_request
|
51
|
+
@curl.headers['Accept'] = 'application/xml'
|
52
|
+
@curl.headers['Content-Type'] = 'application/xml'
|
53
|
+
end
|
54
|
+
|
55
|
+
def xml_payload(xml)
|
56
|
+
xml_request
|
57
|
+
@payload = xml
|
58
|
+
end
|
59
|
+
|
60
|
+
def xml_payload_from_args_hash(root) # APIs have different formatting/hygiene needs...
|
61
|
+
xml_request
|
62
|
+
@payload = @args_hash.to_xml(:root => root, :indent => 0) # .gsub(/(>[ \t\r\n]+<\/)/,"><\/").gsub(/[ \t\r\n]+^/,'')
|
63
|
+
end
|
64
|
+
|
65
|
+
# response utilities:
|
66
|
+
def hash_from_xml_response
|
67
|
+
@response = Hash.from_xml(@raw_response_string)
|
68
|
+
@response ||= {}
|
69
|
+
end
|
70
|
+
|
71
|
+
def hash_from_json_response
|
72
|
+
@response = (ActiveSupport::JSON.decode(@raw_response_string) rescue {})
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def unpack_response
|
78
|
+
# convention is a plain string response
|
79
|
+
# usually it will be an XML or JSON response.
|
80
|
+
# extending classes can/should implement their own unpack_response method to deal with
|
81
|
+
# - unpacking response body xml/json usually to a hash or whatever is naturally consumable by invoker
|
82
|
+
# (see hash_from_xml_response)
|
83
|
+
# - perform additional extraction/conversion,
|
84
|
+
# - trigger response-specific work, ...
|
85
|
+
@response = @raw_response_string
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def success_message
|
91
|
+
"Successful (#{@http_status})"
|
92
|
+
end
|
93
|
+
|
94
|
+
def fail_message
|
95
|
+
"Could not complete request : http response: #{@http_status}, response body : #{@raw_response_string[0..500]}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def invoke_curl_action
|
99
|
+
@curl = Curl::Easy.new(url)
|
100
|
+
curl.timeout = 10
|
101
|
+
invoke_action
|
102
|
+
@http_status = @curl.response_code
|
103
|
+
@raw_response_string = @curl.body_str
|
104
|
+
@raw_response_string ||= ''
|
105
|
+
determine_status
|
106
|
+
unpack_response
|
107
|
+
end
|
108
|
+
|
109
|
+
def inject_status_and_response(simulated_status, simulated_response)
|
110
|
+
@http_status = simulated_status
|
111
|
+
@raw_response_string = 'simulated'
|
112
|
+
determine_status
|
113
|
+
@response = simulated_response
|
114
|
+
@response ||= @raw_response_string
|
115
|
+
end
|
116
|
+
|
117
|
+
def determine_status
|
118
|
+
# enhancement : allow specification/injection of (list of) successful http responses
|
119
|
+
@success = @http_status >= 200 && @http_status < 300
|
120
|
+
@message = success ? success_message : fail_message
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Curbala
|
2
|
+
class Service
|
3
|
+
|
4
|
+
# enhancement : config specifies format of response, action base class supports xml and json config-based unpacking, keep unpack_response base class and extending class implementation
|
5
|
+
# enhancement : config to specify unpack method. altho at service-level, may want flexibility for each action to do its own thing. there's a reasonable strategy somewhere...
|
6
|
+
|
7
|
+
def self.invoke(action_name, args_hash={}, associated_model=nil, logger=nil, simulated_status=200, inject_response=nil)
|
8
|
+
logger ||= self.find_or_create_logger(associated_model)
|
9
|
+
action = eval("#{service_qualifier}::#{action_name.to_s.camelize}.new(service_url_segment(associated_model), env_service_config, args_hash, logger, simulated_status, inject_response)")
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def self.env_service_config
|
15
|
+
config_path = "#{Rails.root rescue '.'}/config/#{config_file}"
|
16
|
+
config_contents = File.open(config_path)
|
17
|
+
config_yml = YAML.load(config_contents)
|
18
|
+
env = (Rails.env rescue 'test')
|
19
|
+
env_yml = config_yml[env]
|
20
|
+
env_yml ||= {}
|
21
|
+
env_yml['simulate'] ||= false
|
22
|
+
env_yml['url'] ||= config_yml['url']
|
23
|
+
env_yml
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.find_or_create_logger(associated_model)
|
27
|
+
logger = (associated_model.logger rescue nil)
|
28
|
+
logger ||= (Rails.logger rescue nil)
|
29
|
+
logger ||= Logger.new(STDOUT)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Curbala
|
2
|
+
module Generators
|
3
|
+
class ActionGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('../template', __FILE__)
|
5
|
+
|
6
|
+
def generate_action
|
7
|
+
|
8
|
+
puts ""
|
9
|
+
install_directory = ask("Enter path to directory where service/action should be installed [app/models] : ")
|
10
|
+
install_directory = 'app/models' if install_directory.blank?
|
11
|
+
|
12
|
+
puts ""
|
13
|
+
service_name = ask("What is the service name for the new action?").downcase
|
14
|
+
generate_and_remember_file("config.yml", "config/#{service_name}.yml")
|
15
|
+
generate_and_remember_file("service.rb", "#{install_directory}/#{service_name}/service.rb")
|
16
|
+
|
17
|
+
puts ""
|
18
|
+
new_action_name = ask("What is the new action name?").downcase
|
19
|
+
generate_and_remember_file("action.rb", "#{install_directory}/#{service_name}/#{new_action_name}.rb")
|
20
|
+
|
21
|
+
puts ""
|
22
|
+
if ask("Generate spec/models/#{service_name}/#{new_action_name}_spec.rb? [Yn]") == "Y"
|
23
|
+
generate_and_remember_file("action_spec.rb", "spec/models/#{service_name}/#{new_action_name}_spec.rb")
|
24
|
+
end
|
25
|
+
|
26
|
+
@generated_files.each do |file_name|
|
27
|
+
gsub_file file_name, "SERVICE_NAME_DOWNCASE", service_name
|
28
|
+
gsub_file file_name, "SERVICE_CLASS_NAME", service_name.classify
|
29
|
+
gsub_file file_name, "ACTION_CLASS_NAME", new_action_name.classify
|
30
|
+
end
|
31
|
+
|
32
|
+
# TODO : chmod's?
|
33
|
+
|
34
|
+
puts ""
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def generate_and_remember_file(source, destination)
|
41
|
+
copy_file source, destination
|
42
|
+
@generated_files ||= []
|
43
|
+
@generated_files << destination
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class SERVICE_CLASS_NAME::ACTION_CLASS_NAME < Curbala::Action
|
2
|
+
|
3
|
+
def action_url_segment
|
4
|
+
# if this is a GET index, GET new or POST create action, probably blank.
|
5
|
+
# for update, show, edit, delete, express id however service expects it (in URL or possibly in payload)
|
6
|
+
|
7
|
+
# * data written/specified in this string must be appropriately url-encoded
|
8
|
+
|
9
|
+
"construct action segment of SERVICE_CLASS_NAME ACTION_CLASS_NAME url in action_url_segment() method at #{__FILE__}:#{__LINE__}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def invoke_action
|
13
|
+
|
14
|
+
# this is where arguments from @args_hash are transformed into url parameters or request payload.
|
15
|
+
# - see Curbala::Action.xml_payload_from_args_hash()
|
16
|
+
# the proper http method is invoked
|
17
|
+
|
18
|
+
# can set action-specific curl options:
|
19
|
+
# curl.timeout = 300
|
20
|
+
# set_payload # see Curbala::Action.set_payload()
|
21
|
+
# xml_request # see Curbala::Action.xml_request()
|
22
|
+
|
23
|
+
# some examples:
|
24
|
+
# curl.http_get
|
25
|
+
# curl.http_post(Curl::PostField.content(:name, args_hash[:name]))
|
26
|
+
# curl.http_put(Curl::PostField.content(:_arg, args_hash[:_arg]))
|
27
|
+
# curl.http_delete
|
28
|
+
|
29
|
+
# APIs have different formatting/hygiene/unpacking needs...
|
30
|
+
# curbala is xml/json agnostic and does not induce any xml/json library bloat
|
31
|
+
# pull in whatever you need to correctly format your request data (REXML, Nokogiri, JSON) from args_hash data
|
32
|
+
# recommend an intermediate class for actions that are constructed and/or unpacked in a similar way
|
33
|
+
|
34
|
+
# some half-hearted support is provided.
|
35
|
+
# see xml_request(), xml_payload(xml) and xml_payload_from_args_hash(root) methods.
|
36
|
+
|
37
|
+
send("implement invoke_action() method in #{__FILE__}:12")
|
38
|
+
end
|
39
|
+
|
40
|
+
# methods you may choose to implement and override default/base-class implementations:
|
41
|
+
|
42
|
+
# def unpack_response
|
43
|
+
# # has a default implementation that just sets @response from @raw_response_string (no xml or json conversion)
|
44
|
+
#
|
45
|
+
# # implementations of this method must transform @raw_response_string into @response
|
46
|
+
# # your service responses will most likely be xml or json
|
47
|
+
# # and need some unpacking into a hash for consumption by controllers, views and other models.
|
48
|
+
#
|
49
|
+
# # @raw_response_string contains curl.body_str
|
50
|
+
#
|
51
|
+
# # see hash_from_xml_response(), hash_from_json_response() methods.
|
52
|
+
# end
|
53
|
+
|
54
|
+
# def success_message
|
55
|
+
# # has a default implementation : extending classes can override to suit app's needs
|
56
|
+
# end
|
57
|
+
|
58
|
+
# def fail_message
|
59
|
+
# # has a default implementation : extending classes can override to suit app's needs
|
60
|
+
# end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe 'SERVICE_CLASS_NAME::ACTION_CLASS_NAME' do
|
4
|
+
before(:each) do
|
5
|
+
@config = {'simulate' => true, 'url' => 'http://base service url/'}
|
6
|
+
(@logger = mock).should_receive(:debug).any_number_of_times
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should source action_url_segment, success_message and simulated response from SERVICE_CLASS_NAME::ACTION_CLASS_NAME implementation when simulated" do
|
10
|
+
Curl::Easy.should_receive(:new).never # simulated : invoke_action() is not invoked.
|
11
|
+
@curbala_action_instance = SERVICE_CLASS_NAME::ACTION_CLASS_NAME.new('service url segment/', @config, {}, @logger)
|
12
|
+
@curbala_action_instance.url.should == "http://base service url/service url segment/#{@curbala_action_instance.action_url_segment}"
|
13
|
+
@curbala_action_instance.success.should be_true
|
14
|
+
@curbala_action_instance.http_status.should == 200
|
15
|
+
@curbala_action_instance.message.should == "Successful (200)"
|
16
|
+
@curbala_action_instance.response.should == "simulated response"
|
17
|
+
end
|
18
|
+
|
19
|
+
describe 'invoke_action' do
|
20
|
+
before(:each) do
|
21
|
+
@args_hash = {}
|
22
|
+
@config['simulate'] = false
|
23
|
+
@expected_url = "http://base service url/service url segment/action url segment"
|
24
|
+
@mocked_curl = mock # (:body_str => @mocked_response)
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "and http response code is 200" do
|
28
|
+
|
29
|
+
it "should indicate success" do
|
30
|
+
# TODO1 : specify @args_hash data for test case
|
31
|
+
|
32
|
+
# TODO2 : construct @mocked_response for test case
|
33
|
+
@mocked_response = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><your-response-root-node>mocked test case response</your-response-root-node>"
|
34
|
+
|
35
|
+
# TODO3 : expectations for work performed in invoke_action()
|
36
|
+
|
37
|
+
# TODO4 : expectations for work done in unpack_response
|
38
|
+
@expected_response_data = @mocked_response
|
39
|
+
|
40
|
+
# Example:
|
41
|
+
# when invoke_action() is implemented as:
|
42
|
+
# curl.http_put(Curl::PostField.content(:test_arg, args_hash['test_arg']).to_s)
|
43
|
+
# then do something like:
|
44
|
+
# @args_hash['test_arg'] = 'test arg value'
|
45
|
+
# Curl::PostField.should_receive(:content).with(:test_arg, 'test arg value').and_return("whatever post field content returns")
|
46
|
+
# @mocked_curl.should_receive(:http_put).with("whatever post field content returns")
|
47
|
+
|
48
|
+
@expected_message = 'Successful (200)'
|
49
|
+
verify_curbala_action(SERVICE_CLASS_NAME::ACTION_CLASS_NAME, 200, be_true)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# TODO : specs for different HTTP stati
|
54
|
+
|
55
|
+
# describe "when http response code is 501" do
|
56
|
+
# end
|
57
|
+
|
58
|
+
def verify_curbala_action(class_under_test, forced_http_status, be_expected_condition)
|
59
|
+
@mocked_curl.should_receive(:body_str).any_number_of_times.and_return(@mocked_response)
|
60
|
+
Curl::Easy.should_receive(:new).with(@expected_url).and_return(@mocked_curl)
|
61
|
+
@mocked_curl.should_receive(:timeout=).with(10)
|
62
|
+
@mocked_curl.should_receive(:response_code).any_number_of_times.and_return(forced_http_status)
|
63
|
+
|
64
|
+
@curbala_instance = class_under_test.new('service url segment/', @config, @args_hash, @logger)
|
65
|
+
|
66
|
+
@curbala_instance.url.should == @expected_url
|
67
|
+
@curbala_instance.message.should == @expected_message
|
68
|
+
@curbala_instance.success.should be_expected_condition
|
69
|
+
@curbala_instance.response.should == @expected_response_data
|
70
|
+
@curbala_instance.http_status.should == forced_http_status
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# tips:
|
2
|
+
# - can specify url at top level and override in environment-specific block as needed.
|
3
|
+
# - be sure http or https is specified as needed for each unsimulated environment.
|
4
|
+
# - block needed for each of your application's environments.
|
5
|
+
# - do not recommend running test unsimulated : unit tests should be hitting external APIs.
|
6
|
+
# - development makes no connectivity assumptions and, by default, runs simulated.
|
7
|
+
# set/manipulate action instance simulated_response implementations as needed.
|
8
|
+
# if service is available in development mode, set 'simulate' to false and specify url.
|
9
|
+
# - standard disclaimer about multiple developers testing concurrently in unsimulated mode : things could get weird...
|
10
|
+
# - can specify additional parameters that an action instance can plug in via its invoke_action() implementation.
|
11
|
+
# handy for plugging in other environment-specific, or somehow appropriately specifiable, goo.
|
12
|
+
|
13
|
+
# url: http://SERVICE_NAME_DOWNCASE base service url for all environments
|
14
|
+
|
15
|
+
test:
|
16
|
+
simulate: true
|
17
|
+
url: http://test SERVICE_NAME_DOWNCASE base service url
|
18
|
+
|
19
|
+
development:
|
20
|
+
simulate: true
|
21
|
+
url: http://development SERVICE_NAME_DOWNCASE base service url
|
22
|
+
|
23
|
+
production:
|
24
|
+
simulate: false
|
25
|
+
url: http://TODO__SPECIFY_LEFT_JUSTIFIED_ENVIRONMENT_DEPENDENT_PORTION_OF_SERVICE_URL_IN___config__SERVICE_NAME_DOWNCASE.yml
|
26
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class SERVICE_CLASS_NAME::Service < Curbala::Service
|
2
|
+
|
3
|
+
def self.service_url_segment(associated_model)
|
4
|
+
"/TODO__SPECIFY_ENVIRONMENT_INDEPENDENT_PORTION_OF_SERVICE_URL_IN___SERVICE_CLASS_NAME_Service__service_url_segment__method/"
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.service_qualifier
|
8
|
+
'SERVICE_CLASS_NAME'
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.config_file # see self.env_service_config()
|
12
|
+
'SERVICE_NAME_DOWNCASE.yml'
|
13
|
+
end
|
14
|
+
|
15
|
+
# def self.env_service_config
|
16
|
+
# # a default YAML-based implementation is provided :
|
17
|
+
# # YAML.load(File.open("#{Rails.root rescue '.'}/config/#{config_file}.yml"))
|
18
|
+
#
|
19
|
+
# # if you prefer HAML/XML/... or don't need a config file,
|
20
|
+
# # override implementation with logic that meets your needs
|
21
|
+
# # and remove generated config/SERVICE_NAME_DOWNCASE.yml file.
|
22
|
+
# end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe 'Curbala::Action' do
|
5
|
+
before(:each) do
|
6
|
+
@args_hash = {:test_arg => 'test arg value'}
|
7
|
+
@expected_url = 'test base service url/' + 'service url segment/action url segment'
|
8
|
+
@expected_message = 'Successful (200)'
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'initialize' do
|
12
|
+
describe 'and not simulated' do
|
13
|
+
before(:each) { @mocked_response_xml = "<?xml version='1.0' encoding='UTF-8'?><root>mocked response fields</root>" }
|
14
|
+
|
15
|
+
it 'should use base implementation of unpack_response when extending class does not implement unpack_response' do
|
16
|
+
unsimulated_curbala_action(CurbalaActionTestClass, @mocked_response_xml)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should use implemented unpack_response method when extending class implements unpack_response' do
|
20
|
+
@expected_message = 'success message (overriden)'
|
21
|
+
unsimulated_curbala_action(CurbalaActionTestClassWithOverridenImplementations, {'root' => 'mocked response fields'})
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should indicate failure and include exception message in action message when exception occurs' do
|
25
|
+
xmsg = 'message from exception'
|
26
|
+
Curl::Easy.should_receive(:new).with(@expected_url).and_raise(RuntimeError.new(xmsg))
|
27
|
+
@expected_message = "Service Not Available: #{xmsg}"
|
28
|
+
it_should_do_expected_curbala_action_stuff(CurbalaActionTestClass, config(false), -1, 'exception occurred in action processing', be_false)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
[200, 299].each do |http_status|
|
33
|
+
it "should indicate success when http response code is #{http_status}" do
|
34
|
+
@expected_message = "Successful (#{http_status})"
|
35
|
+
it_should_simulate_curbala_call(CurbalaActionTestClass, config, http_status, {'root' => 'simulated response fields'})
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
[199, 300].each do |http_status|
|
40
|
+
it "should indicate failure when http response code is #{http_status}" do
|
41
|
+
@expected_message = "Could not complete request : http response: #{http_status}, response body : simulated"
|
42
|
+
it_should_simulate_curbala_call(CurbalaActionTestClass, config, http_status, {'root' => 'simulated failure response fields'}, be_false)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def it_should_simulate_curbala_call(class_under_test, config_hash, expected_response_code, expected_response_data, be_expected_condition = be_true)
|
47
|
+
Curl::Easy.should_receive(:new).never
|
48
|
+
Curl::PostField.should_receive(:content).never
|
49
|
+
Hash.should_receive(:from_xml).never
|
50
|
+
it_should_do_expected_curbala_action_stuff(class_under_test, config_hash, expected_response_code, expected_response_data, be_expected_condition)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# describe 'url construction utilities' do
|
55
|
+
# end
|
56
|
+
|
57
|
+
describe 'xml request utilities' do
|
58
|
+
before(:each) { unsimulated_curbala_action(CurbalaActionTestClass, "") }
|
59
|
+
|
60
|
+
describe 'xml_request' do
|
61
|
+
it 'should set curl header Accept and Content-Type values to application/xml' do
|
62
|
+
@mocked_curl.headers['Accept'] = 'not application/xml'
|
63
|
+
@mocked_curl.headers['Content-Type'] = 'not application/xml'
|
64
|
+
@curbala_instance.xml_request
|
65
|
+
@mocked_curl.headers['Accept'].should == 'application/xml'
|
66
|
+
@mocked_curl.headers['Content-Type'].should == 'application/xml'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe 'xml_payload(xml)' do
|
71
|
+
it 'should invoke xml_request and set payload from input payload' do
|
72
|
+
@curbala_instance.should_receive(:xml_request)
|
73
|
+
@curbala_instance.xml_payload('my xml payload')
|
74
|
+
@curbala_instance.payload.should == 'my xml payload'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe 'xml_payload_from_args_hash(root)' do
|
79
|
+
it 'should invoke xml_request and set payload from input payload' do
|
80
|
+
@curbala_instance.should_receive(:xml_request)
|
81
|
+
@curbala_instance.xml_payload_from_args_hash('my xml root')
|
82
|
+
@curbala_instance.payload.should == "<?xml version=\"1.0\" encoding=\"UTF-8\"?><my-xml-root><test-arg>test arg value</test-arg></my-xml-root>"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe 'xml response utilities' do
|
88
|
+
before(:each) { unsimulated_curbala_action(CurbalaActionTestClass, "") }
|
89
|
+
|
90
|
+
describe 'hash_from_xml_response' do
|
91
|
+
it 'should set response to a hash representation of xml in raw_response_string' do
|
92
|
+
@curbala_instance.raw_response_string = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><response-root><response-object1><response-field1>test arg value</response-field1><response-field2>field2 value</response-field2></response-object1></response-root>"
|
93
|
+
@curbala_instance.hash_from_xml_response
|
94
|
+
@curbala_instance.response.should == { "response_root" => {"response_object1"=>{"response_field1"=>"test arg value", "response_field2"=>"field2 value"}} }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe 'hash_from_json_response' do
|
99
|
+
it 'should set response to a hash representation of json in raw_response_string' do
|
100
|
+
@curbala_instance.raw_response_string = '[{"sf": "HTTP", "lfs": [{"lf": "hypertext transfer protocol", "freq": 6, "since": 1995, "vars": [{"lf": "Hypertext Transfer Protocol", "freq": 3, "since": 1996}, {"lf": "hypertext transfer protocol", "freq": 3, "since": 1995}]}]}]'
|
101
|
+
@curbala_instance.hash_from_json_response
|
102
|
+
@curbala_instance.response.should == [{"sf"=>"HTTP", "lfs"=>[{"freq"=>6, "vars"=>[{"freq"=>3, "lf"=>"Hypertext Transfer Protocol", "since"=>1996}, {"freq"=>3, "lf"=>"hypertext transfer protocol", "since"=>1995}], "lf"=>"hypertext transfer protocol", "since"=>1995}]}]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def unsimulated_curbala_action(test_class, expected_response)
|
108
|
+
@mocked_curl = mock(:body_str => @mocked_response_xml)
|
109
|
+
Curl::Easy.should_receive(:new).with(@expected_url).and_return(@mocked_curl)
|
110
|
+
@mocked_curl.should_receive(:timeout=).with(10)
|
111
|
+
Curl::PostField.should_receive(:content).with(:test_arg, 'test arg value').and_return('whatever post field content returns')
|
112
|
+
@mocked_curl.should_receive(:http_put).with('whatever post field content returns')
|
113
|
+
@mocked_curl.should_receive(:response_code).any_number_of_times.and_return(200)
|
114
|
+
|
115
|
+
it_should_do_expected_curbala_action_stuff test_class, config(false), 200, expected_response
|
116
|
+
|
117
|
+
headers = {}
|
118
|
+
@mocked_curl.should_receive(:headers).any_number_of_times.and_return(headers)
|
119
|
+
@curbala_instance.xml_payload_from_args_hash 'a root node'
|
120
|
+
@curbala_instance.payload.should == "<?xml version=\"1.0\" encoding=\"UTF-8\"?><a-root-node><test-arg>test arg value</test-arg></a-root-node>"
|
121
|
+
headers.should == {"Content-Type"=>"application/xml", "Accept"=>"application/xml"}
|
122
|
+
end
|
123
|
+
|
124
|
+
def it_should_do_expected_curbala_action_stuff(class_under_test, config_hash, expected_response_code, expected_response_data, be_expected_condition = be_true)
|
125
|
+
curbala_action(class_under_test, config_hash, expected_response_code, expected_response_data)
|
126
|
+
@curbala_instance.url.should == @expected_url
|
127
|
+
@curbala_instance.message.should == @expected_message
|
128
|
+
@curbala_instance.success.should be_expected_condition
|
129
|
+
@curbala_instance.response.should == expected_response_data
|
130
|
+
@curbala_instance.http_status.should == expected_response_code
|
131
|
+
end
|
132
|
+
|
133
|
+
def curbala_action(class_under_test, config_hash, expected_response_code, expected_response_data)
|
134
|
+
(logger = mock).should_receive(:debug).any_number_of_times # TODO : tighten this up?
|
135
|
+
@curbala_instance = class_under_test.new('service url segment/', config_hash, @args_hash, logger, expected_response_code, expected_response_data)
|
136
|
+
end
|
137
|
+
|
138
|
+
def config(simulate = true)
|
139
|
+
{'simulate' => simulate, 'url' => 'test base service url/'}
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
class CurbalaActionTestClass < Curbala::Action
|
145
|
+
def action_url_segment; "action url segment"; end
|
146
|
+
def invoke_action; curl.http_put(Curl::PostField.content(:test_arg, args_hash[:test_arg])); end
|
147
|
+
end
|
148
|
+
|
149
|
+
class CurbalaActionTestClassWithOverridenImplementations < CurbalaActionTestClass
|
150
|
+
def unpack_response
|
151
|
+
hash_from_xml_response
|
152
|
+
end
|
153
|
+
|
154
|
+
def success_message
|
155
|
+
"success message (overriden)"
|
156
|
+
end
|
157
|
+
|
158
|
+
def fail_message
|
159
|
+
"fail message (overriden)"
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Curbala::Service' do
|
4
|
+
describe 'invoke and no/nil logger input' do
|
5
|
+
before(:each) { @mocked_config = { 'test' => {'simulate' => true, 'url' => 'service url segment'} } }
|
6
|
+
|
7
|
+
it 'should use associated model logger to invoke new on proper Curbala::Action class with expected arguments, and return the action instance when no logger input and associated model has a logger' do
|
8
|
+
@mocked_model = mock(:logger => 'model logger')
|
9
|
+
verify_invoked_with 'model logger'
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'and associated model does not have a logger' do
|
13
|
+
before(:each) { @mocked_model = mock(:logger => nil) }
|
14
|
+
|
15
|
+
it 'should use Rails logger when in a Rails environment' do
|
16
|
+
unless defined?(Rails) # is this beggin for trouble?
|
17
|
+
class Rails; end
|
18
|
+
end
|
19
|
+
Rails.should_receive(:logger).and_return('rails logger')
|
20
|
+
verify_invoked_with 'rails logger'
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should create a new Logger to STDOUT when not in a Rails environment' do
|
24
|
+
verify_invoked_with_new_logger
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'and config file has url specified at top-level' do
|
29
|
+
before(:each) { @mocked_config = {'url' => 'top level config service url segment'} }
|
30
|
+
|
31
|
+
it 'should use top-level url and not simulate when config file does not have a block for Rails.env (test)' do
|
32
|
+
@expected_config = {'simulate' => false, 'url' => 'top level config service url segment'}
|
33
|
+
verify_invoked_with_new_logger
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should use environment-specific url when specified in environment block of config file' do
|
37
|
+
@mocked_config['test'] = {'simulate' => true, 'url' => 'service url segment'}
|
38
|
+
verify_invoked_with_new_logger
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def verify_invoked_with_new_logger
|
43
|
+
Logger.should_receive(:new).with(STDOUT).and_return('new logger')
|
44
|
+
verify_invoked_with 'new logger'
|
45
|
+
end
|
46
|
+
|
47
|
+
def verify_invoked_with(logger)
|
48
|
+
@expected_config ||= @mocked_config['test']
|
49
|
+
File.should_receive(:open).with("./config/test curbala config file.yml").and_return('mocked open file') # TODO : Rails.root +
|
50
|
+
YAML.should_receive(:load).with('mocked open file').and_return(@mocked_config)
|
51
|
+
TestCurbalaActionClass::SomeAction.should_receive(:new).with('test_curbala_service_url_segment', @expected_config, 'args_hash', logger, 200, nil).and_return('test service response')
|
52
|
+
TestCurbala::Service.invoke(:some_action, 'args_hash', @mocked_model).should == 'test service response'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module TestCurbalaActionClass
|
58
|
+
class SomeAction < Curbala::Action
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module TestCurbala
|
63
|
+
class Service < Curbala::Service
|
64
|
+
def self.service_url_segment(business)
|
65
|
+
"test_curbala_service_url_segment"
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.service_qualifier
|
69
|
+
'TestCurbalaActionClass'
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.config_file
|
73
|
+
'test curbala config file.yml'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: curbala
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Frederick Fix
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.10.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.10.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rails
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '3.2'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '3.2'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: curb
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.8.1
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.8.1
|
62
|
+
description: Does you application invoke services that have different URLs in different
|
63
|
+
Rails environments? Would you like to log the application input to each service
|
64
|
+
call, have fine grain control over API-specific data encoding and decoding, and
|
65
|
+
log responses for each service call? Are some of your service calls exhibiting
|
66
|
+
a 'long line' code odor? Do your API developers test their calls using curl commands? This
|
67
|
+
little ditty remedied these symptoms in our system and, most imporantly, expedited
|
68
|
+
our rails app development and troubleshooting with internal and external API teams.
|
69
|
+
email: rickfix80004@gmail.com
|
70
|
+
executables: []
|
71
|
+
extensions: []
|
72
|
+
extra_rdoc_files: []
|
73
|
+
files:
|
74
|
+
- lib/curbala/action.rb
|
75
|
+
- lib/curbala/service.rb
|
76
|
+
- lib/curbala.rb
|
77
|
+
- lib/generators/curbala/action/action_generator.rb
|
78
|
+
- lib/generators/curbala/action/template/action.rb
|
79
|
+
- lib/generators/curbala/action/template/action_spec.rb
|
80
|
+
- lib/generators/curbala/action/template/config.yml
|
81
|
+
- lib/generators/curbala/action/template/service.rb
|
82
|
+
- lib/generators/curbala/action/USAGE
|
83
|
+
- spec/curbala/action_spec.rb
|
84
|
+
- spec/curbala/service_spec.rb
|
85
|
+
- spec/spec.opts
|
86
|
+
- spec/spec_helper.rb
|
87
|
+
- CHANGELOG.rdoc
|
88
|
+
- Gemfile
|
89
|
+
- LICENSE
|
90
|
+
- Rakefile
|
91
|
+
- README.rdoc
|
92
|
+
- init.rb
|
93
|
+
homepage: http://github.com/rickfix/curbala
|
94
|
+
licenses: []
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ! '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.3.4
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project: curbala
|
113
|
+
rubygems_version: 1.8.24
|
114
|
+
signing_key:
|
115
|
+
specification_version: 3
|
116
|
+
summary: Curb client wrapper which encourages DRY implementations of, and provides
|
117
|
+
logging for, external REST/API/Service calls.
|
118
|
+
test_files: []
|