curbala 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,4 @@
1
+ 0.0.1 2012-07-26
2
+
3
+ * first whack
4
+
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,2 @@
1
+ require 'curbala/action'
2
+ require 'curbala/service'
@@ -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,5 @@
1
+ Description:
2
+ The curbala:action generator creates a new Curbala::Action class in app/models/curbala/services directory.
3
+ You can move this file anywhere you want as long as it is in the load path.
4
+ TODO : args or prompts?
5
+
@@ -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
@@ -0,0 +1,2 @@
1
+ --color
2
+ --backtrace
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'curb'
4
+ require 'curbala'
5
+
6
+ RSpec.configure do |config|
7
+ config.treat_symbols_as_metadata_keys_with_true_values = true
8
+ config.run_all_when_everything_filtered = true
9
+ end
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: []