tapi 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ module TAPI
2
+
3
+ module Utils
4
+
5
+ module_function
6
+
7
+ def coerce_date(date)
8
+ case date
9
+ when Date then date
10
+ when String then parse_date(date)
11
+ when NilClass then nil
12
+ else
13
+ raise TypeError, "cannot coerce #{date.inspect}", caller
14
+ end
15
+ end
16
+
17
+ def parse_date(date)
18
+ Date.strptime(date, '%d.%m.%Y')
19
+ rescue ArgumentError
20
+ begin
21
+ Date.strptime(date, '%Y-%m-%d')
22
+ rescue ArgumentError
23
+ raise TypeError, "cannot parse #{date.inspect}", caller
24
+ end
25
+ end
26
+
27
+ def append_query(url, hash)
28
+ pairs = hash.to_a.sort {|a, b| a.first.to_s <=> b.first.to_s}
29
+ query_elements =
30
+ pairs.inject([]) do |accumulator, key_value|
31
+ key, value = key_value
32
+ case value
33
+ when Array
34
+ value.each do |v|
35
+ accumulator << "#{key}[]=#{v}"
36
+ end
37
+ else
38
+ accumulator << "#{key}=#{value}"
39
+ end
40
+ accumulator
41
+ end
42
+ if query_elements.any?
43
+ attributes = URI.escape(query_elements.join('&'))
44
+ join_char = url.include?('?') ? '&' : '?'
45
+ url + join_char + attributes
46
+ else
47
+ url
48
+ end
49
+ end
50
+
51
+ def symbolize_keys(data)
52
+ case data
53
+ when Hash
54
+ data.inject({}){|acc, pair| acc[pair.first.to_sym] = symbolize_keys(pair.last); acc}
55
+ when Array
56
+ data.map {|e| symbolize_keys(e)}
57
+ else
58
+ data
59
+ end
60
+ end
61
+
62
+
63
+ end
64
+ end
@@ -0,0 +1,58 @@
1
+ module TAPI
2
+
3
+ module Validations
4
+
5
+ def has_errors?
6
+ errors.any?
7
+ end
8
+
9
+ def add_error(name, message)
10
+ (errors[name] ||= []) << message
11
+ end
12
+
13
+ def validations
14
+ @validations ||= []
15
+ end
16
+
17
+ def inherited_validations
18
+ if superclass.respond_to?(:validations)
19
+ superclass.validations + validations
20
+ else
21
+ validations
22
+ end
23
+ end
24
+
25
+ def validate(&block)
26
+ validations << block
27
+ end
28
+
29
+ def validates_presence_of(name, message)
30
+ validate do
31
+ if send(name).blank?
32
+ add_error(name, message)
33
+ end
34
+ end
35
+ end
36
+
37
+ def validates_date_format_of(name, message)
38
+ validate do
39
+ begin
40
+ Utils.coerce_date(send(name))
41
+ rescue TypeError
42
+ add_error name, message
43
+ end
44
+ end
45
+ end
46
+
47
+ def validates_numericality_of(name, message)
48
+ validate do
49
+ number = send(name)
50
+ unless number.is_a?(Fixnum) || /^[0-9]+$/ =~ number
51
+ add_error name, message
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,320 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ describe TAPI::V3::Client, " utility methods" do
5
+
6
+ it 'should build a query string' do
7
+ TAPI::Utils.append_query('url', {}).should == 'url'
8
+ TAPI::Utils.append_query('url', {:param1 => 1, :param2 => 'zwo'}).should == 'url?param1=1&param2=zwo'
9
+ TAPI::Utils.append_query('url?already=here', {:param1 => 1, :param2 => 'zwo'}).should == 'url?already=here&param1=1&param2=zwo'
10
+ TAPI::Utils.append_query('url', {:array => [1,2]}).should == 'url?array[]=1&array[]=2'
11
+ TAPI::Utils.append_query('url', {:umlaut => 'ü'}).should == 'url?umlaut=%C3%BC'
12
+ end
13
+
14
+ it 'should symbolize keys in a hash' do
15
+ data_in = {'i_am' => {'a_nested' => 'hash'}, 'with' => {'an_array' => [2, 3, 4]}}
16
+ data_out = {:i_am => {:a_nested => 'hash'}, :with => {:an_array => [2, 3, 4]}}
17
+ TAPI::Utils.symbolize_keys(data_in).should == data_out
18
+ end
19
+
20
+ it 'should have a default logger' do
21
+ TAPI::V3::Client.logger.class.should == Logger
22
+ TAPI::V3::Client.new({}).logger.class.should == Logger
23
+ end
24
+
25
+ it 'should have a configurable logger' do
26
+ logger = mock('Logger').as_null_object
27
+ TAPI::V3::Client.logger = logger
28
+ TAPI::V3::Client.logger.should == logger
29
+ TAPI::V3::Client.new({}).logger.should == logger
30
+ end
31
+
32
+ it "should translate HTTP errors" do
33
+ curl = mock('Curl', :response_code => 404)
34
+ curl.stub!(:url, nil)
35
+ curl.stub!(:body_str, nil)
36
+ lambda {TAPI::V3::Client.check_for_errors(curl)}.should raise_error TAPI::NotFoundError
37
+ end
38
+
39
+ it "should raise an error for an unknown HTTP errors" do
40
+ curl = mock('Curl', :response_code => 517)
41
+ lambda {TAPI::V3::Client.check_for_errors(curl)}.should raise_error
42
+ end
43
+
44
+ it "should provide context for HTTP errors" do
45
+ curl = mock('Curl',
46
+ :response_code => 412,
47
+ :url => 'superdope.curl',
48
+ :body_str => 'dead-bodies')
49
+
50
+ error = nil
51
+
52
+ begin
53
+ TAPI::V3::Client.check_for_errors(curl)
54
+ rescue TAPI::Error => e
55
+ error = e
56
+ end
57
+
58
+ error.should_not be_nil
59
+ error.response_code.should == 412
60
+ error.response_body.should == 'dead-bodies'
61
+ error.request_url.should == 'superdope.curl'
62
+ end
63
+
64
+ end
65
+
66
+ describe TAPI::V3::Client, " retrieving remote data" do
67
+
68
+ it 'should have configurable authentication' do
69
+ TAPI::V3::Client.http_authentication.should == nil
70
+ TAPI::V3::Client.config = {:http_user_name => 'username', :http_password => 'password'}
71
+ TAPI::V3::Client.http_authentication.should == 'username:password'
72
+ TAPI::V3::Client.config = nil
73
+ end
74
+
75
+ it 'should retrieve data via post' do
76
+ curl = mock('Curl')
77
+ curl.should_receive(:http_post)
78
+ curl.should_receive(:body_str).and_return('the_body')
79
+ JSON.should_receive(:parse).with('the_body').and_return('the_data')
80
+ Curl::Easy.should_receive(:new).and_return(curl)
81
+ TAPI::V3::Client.should_receive(:check_for_errors).with(curl)
82
+ TAPI::V3::Client.should_receive(:new).with('the_data', nil, true)
83
+ TAPI::V3::Client.new_from_post('the_url', {:a_param => :yeah})
84
+ end
85
+
86
+ it 'should use authentication options when posting' do
87
+ curl = mock('Curl')
88
+ curl.should_receive(:http_post)
89
+ curl.should_receive(:userpwd=).with('the_authentication')
90
+ curl.should_receive(:body_str).and_return('the_body')
91
+ JSON.should_receive(:parse).with('the_body').and_return('the_data')
92
+ Curl::Easy.should_receive(:new).and_return(curl)
93
+ TAPI::V3::Client.should_receive(:http_authentication).and_return('the_authentication')
94
+ TAPI::V3::Client.should_receive(:check_for_errors).with(curl)
95
+ TAPI::V3::Client.should_receive(:new).with('the_data', nil, true)
96
+ TAPI::V3::Client.new_from_post('the_url', {:a_param => :yeah})
97
+ end
98
+
99
+ it 'should use authentication options when getting' do
100
+ curl = mock('Curl', :header_str => 'ETag the_etag')
101
+ curl.should_receive(:http_get)
102
+ curl.should_receive(:userpwd=).with('the_authentication')
103
+ curl.should_receive(:body_str).and_return('the_body')
104
+ JSON.should_receive(:parse).with('the_body').and_return('the_data')
105
+ Curl::Easy.should_receive(:new).and_return(curl)
106
+ TAPI::V3::Client.should_receive(:http_authentication).and_return('the_authentication')
107
+ TAPI::V3::Client.should_receive(:check_for_errors).with(curl)
108
+ TAPI::V3::Client.should_receive(:new).with('the_data', 'the_etag', true)
109
+ TAPI::V3::Client.new_from_get('the_url', {:a_param => :yeah})
110
+ end
111
+
112
+ it 'should retrieve data via get without an etag given' do
113
+ curl = mock('Curl', :header_str => 'ETag the_etag')
114
+ curl.should_receive(:http_get)
115
+ curl.should_receive(:body_str).and_return('the_body')
116
+ JSON.should_receive(:parse).with('the_body').and_return('the_data')
117
+ TAPI::Utils.should_receive(:append_query).with('the_url', {:a_param => :yeah}).and_return('the_url?a_param=yeah')
118
+ Curl::Easy.should_receive(:new).with('the_url?a_param=yeah').and_return(curl)
119
+ TAPI::V3::Client.should_receive(:check_for_errors).with(curl)
120
+ TAPI::V3::Client.should_receive(:new).with('the_data', 'the_etag', true).and_return('the_client')
121
+ TAPI::V3::Client.new_from_get('the_url', {:a_param => :yeah}).should == ['the_client', 'the_etag']
122
+ end
123
+
124
+ it 'should retrieve data via get with an non-matching etag given' do
125
+ curl = mock('Curl', :header_str => 'ETag remote_etag')
126
+ curl.should_receive(:http_get)
127
+ curl.should_receive(:body_str).and_return('the_body')
128
+ curl.should_receive(:headers).and_return({})
129
+ JSON.should_receive(:parse).with('the_body').and_return('the_data')
130
+ TAPI::Utils.should_receive(:append_query).with('the_url', {:a_param => :yeah}).and_return('the_url?a_param=yeah')
131
+ Curl::Easy.should_receive(:new).with('the_url?a_param=yeah').and_return(curl)
132
+ TAPI::V3::Client.should_receive(:check_for_errors).with(curl)
133
+ TAPI::V3::Client.should_receive(:new).with('the_data', 'remote_etag', true).and_return('the_client')
134
+ TAPI::V3::Client.new_from_get('the_url', {:a_param => :yeah}, 'etag').should == ['the_client', 'remote_etag']
135
+ end
136
+
137
+ it 'should retrieve data via get with no etag given' do
138
+ curl = mock('Curl', :header_str => 'nothing in here')
139
+ curl.should_receive(:http_get)
140
+ curl.should_receive(:body_str).and_return('the_body')
141
+ curl.should_receive(:headers).and_return({})
142
+ JSON.should_receive(:parse).with('the_body').and_return('the_data')
143
+ TAPI::Utils.should_receive(:append_query).with('the_url', {:a_param => :yeah}).and_return('the_url?a_param=yeah')
144
+ Curl::Easy.should_receive(:new).with('the_url?a_param=yeah').and_return(curl)
145
+ TAPI::V3::Client.should_receive(:check_for_errors).with(curl)
146
+ TAPI::V3::Client.should_receive(:new).with('the_data', nil, true).and_return('the_client')
147
+ TAPI::V3::Client.new_from_get('the_url', {:a_param => :yeah}, 'etag').should == ['the_client', nil]
148
+ end
149
+
150
+ it 'should retrieve data via get with an matching etag given' do
151
+ curl = mock('Curl', :header_str => 'ETag the_etag')
152
+ curl.should_receive(:http_get)
153
+ curl.should_not_receive(:body_str)
154
+ curl.should_receive(:headers).and_return({})
155
+ JSON.should_not_receive(:parse)
156
+ TAPI::Utils.should_receive(:append_query).with('the_url', {:a_param => :yeah}).and_return('the_url?a_param=yeah')
157
+ Curl::Easy.should_receive(:new).with('the_url?a_param=yeah').and_return(curl)
158
+ TAPI::V3::Client.should_receive(:check_for_errors).with(curl)
159
+ TAPI::V3::Client.should_not_receive(:new)
160
+ TAPI::V3::Client.new_from_get('the_url', {:a_param => :yeah}, 'the_etag').should == [nil, 'the_etag']
161
+ end
162
+
163
+ end
164
+
165
+ TAPI::V3::Data = Class.new(TAPI::V3::Client)
166
+ TAPI::V3::NewClient = Class.new
167
+ TAPI::V3::ArrayHashElement = Class.new(TAPI::V3::Client)
168
+
169
+ describe TAPI::V3::Client, " dynamic methods" do
170
+
171
+ before(:each) do
172
+ @data_hash = {
173
+ :search =>
174
+ {
175
+ :resources =>
176
+ {
177
+ :remote1_url => 'remote1_url',
178
+ :remote2_url => 'remote2_url'
179
+ },
180
+ :data =>
181
+ {
182
+ :a_hash => {:some => :data},
183
+ :array_elements => [1, 2, 3],
184
+ :a_value => 'value',
185
+ :nil => nil,
186
+ :array_hash_elements => [{:the => :one}, {:the => :other}]
187
+ }
188
+ }
189
+ }
190
+
191
+ @client = TAPI::V3::Client.new(@data_hash, nil, true)
192
+ @client.class_mapping[:some_key] = TAPI::V3::NewClient
193
+ @client.class_mapping[:data] = TAPI::V3::Data
194
+ @client.class_mapping[:array_hash_elements] = TAPI::V3::ArrayHashElement
195
+
196
+ def @client.remote_cache=(v)
197
+ @remote_cache = v
198
+ end
199
+ end
200
+
201
+ it 'should return the initial hash' do
202
+ @client.to_hash.should == @data_hash
203
+ end
204
+
205
+ it 'should raise an error when a unknown method is called' do
206
+ lambda {@client.search.giveme}.should raise_error NoMethodError
207
+ end
208
+
209
+ it 'should return nil if a key is present but value is nil' do
210
+ @client.search.data.nil.should == nil
211
+ end
212
+
213
+ it 'should raise an error when a unknown method is called' do
214
+ lambda {@client.search.fetch_giveme}.should raise_error NoMethodError
215
+ end
216
+
217
+ it 'should instanciate a new client with a sub-hash' do
218
+ @client.search.class.should == TAPI::V3::Client
219
+ end
220
+
221
+ it 'should instanciate a new client with a sub-sub-hash' do
222
+ @client.search.data.to_hash.should == @data_hash[:search][:data]
223
+ end
224
+
225
+ it 'should return a plain value' do
226
+ @client.search.data.a_value.should == 'value'
227
+ end
228
+
229
+ it 'should not shortcut to a multiple subdocuments' do
230
+ client = TAPI::V3::Client.new(:key => { :value => 'value' }, :second => { :value => 'value' })
231
+ lambda { client.value }.should raise_error NoMethodError
232
+ end
233
+
234
+ it 'should instanciate a set of new client with an array' do
235
+ @client.search.data.array_elements == [1, 2, 3]
236
+ end
237
+
238
+ it 'should know its attributes' do
239
+ @client.attributes == ['search']
240
+ @client.search.attributes == ['resources', 'data']
241
+ end
242
+
243
+ it 'should know its urls' do
244
+ expect = {:remote1_url => 'remote1_url', :remote2_url => 'remote2_url'}
245
+ @client.urls.should == expect
246
+ @client.urls.should == expect
247
+ @client.search.data.urls.should == {}
248
+ end
249
+
250
+ it 'should know its urls' do
251
+ @client.remote_calls.should == ["fetch_remote1", "fetch_remote2"]
252
+ end
253
+
254
+ it 'should be able to call a remote method' do
255
+ @client.should_receive(:get).with('remote1_url', TAPI::V3::Client, {})
256
+ @client.fetch_remote1
257
+ end
258
+
259
+ it 'should be able to call a remote method with options' do
260
+ options = {:please_consider => :this}
261
+ @client.should_receive(:get).with('remote1_url', TAPI::V3::Client, options)
262
+ @client.fetch_remote1(options)
263
+ end
264
+
265
+ it 'should find a client class' do
266
+ @client.send(:client_class, 'some_key').should == TAPI::V3::NewClient
267
+ end
268
+
269
+ it 'should instanciate with a client class' do
270
+ @client.search.data.class.should == TAPI::V3::Data
271
+ end
272
+
273
+ it 'should instanciate with an array of client classes' do
274
+ array = @client.search.data.array_hash_elements
275
+ array.length.should == 2
276
+ array.map(&:class).uniq.should == [TAPI::V3::ArrayHashElement]
277
+ end
278
+
279
+ it 'should generate url keys' do
280
+ @client.send(:url_key, 'lala').should == nil
281
+ @client.send(:url_key, 'fetch_lala').should == 'lala'
282
+ end
283
+
284
+ it 'should generate cache keys' do
285
+ k1 = @client.send(:cache_key, 'url1', {:opt1 => 1})
286
+ k2 = @client.send(:cache_key, 'url1', {:opt1 => 1, :opt2 => 2})
287
+ k3 = @client.send(:cache_key, 'url1', {:opt2 => 2, :opt1 => 1})
288
+ k1.should_not == k2
289
+ k2.should == k3
290
+ end
291
+
292
+ it 'should get data from remote if the cache is empty' do
293
+ @client.remote_cache = {}
294
+ @client.class.should_receive(:new_from_get).with('the_url', {:instanciate_as => 'the_class'}, nil).and_return(['remote_data', 'remote_etag'])
295
+ @client.send(:get, 'the_url', 'the_class').should == 'remote_data'
296
+ end
297
+
298
+ it 'should get data from cache if it is cached' do
299
+ @client.stub!(:cache_key => 'the_key')
300
+ @client.remote_cache = {'the_key' => {:etag => 'the_etag', :data => 'cached_data'}}
301
+ @client.class.should_receive(:new_from_get).with('the_url', {:instanciate_as => 'the_class'}, 'the_etag').and_return(['remote_data', 'the_etag'])
302
+ @client.send(:get, 'the_url', 'the_class').should == 'cached_data'
303
+ end
304
+
305
+ it 'should return a cached reply if :skip_refresh option is given' do
306
+ @client.stub!(:cache_key => 'the_key')
307
+ @client.remote_cache = {'the_key' => {:etag => 'the_etag', :data => 'cached_data'}}
308
+ @client.class.should_not_receive(:new_from_get)
309
+ @client.send(:get, 'the_url', 'the_class', :skip_refresh => true).should == 'cached_data'
310
+ end
311
+
312
+ it 'should get data from remote if the etag is not in the cache' do
313
+ @client.stub!(:cache_key => 'the_key')
314
+ @client.remote_cache = {'the_key' => {:etag => 'local_etag', :data => 'cached_data'}}
315
+ @client.class.should_receive(:new_from_get).with('the_url', {:instanciate_as => 'the_class'}, 'local_etag').and_return(['remote_data', 'remote_etag'])
316
+ @client.send(:get, 'the_url', 'the_class').should == 'remote_data'
317
+ end
318
+
319
+ end
320
+
@@ -0,0 +1,27 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe TAPI::V3::Configurable, " configuration" do
4
+ before(:each) do
5
+ @klass = Class.new
6
+ @klass.send(:include, TAPI::V3::Configurable)
7
+ end
8
+
9
+ it 'should not raise an error if it is configured' do
10
+ @klass.config = {}
11
+ lambda { @klass.config }.should_not raise_error
12
+ end
13
+
14
+ it 'should remember its configuration' do
15
+ config = {:config_data => :it_is}
16
+ @klass.config = config
17
+ @klass.config.should == config
18
+ end
19
+
20
+ it 'should make the configuration accesible to its instances' do
21
+ config = {:config_data => :it_is}
22
+ @klass.config = config
23
+ @klass.new.config.should == config
24
+ end
25
+
26
+ end
27
+