frenetic 0.0.20.alpha.1 → 0.0.20.alpha.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -91,10 +91,17 @@ the API contains, it can't navigate around it or parse any of it's responses.
91
91
  This response will be requested by Frenetic whenever a call to
92
92
  `YourAPI.description` is made.
93
93
 
94
- **Note:** It is highly advised that you implement some sort of caching in both
95
- your API server as well as your API client. Refer to the [Caching][caching] section for
96
- more information.
94
+ **Note:** It is highly advised that your API return Cache-Control headers in
95
+ this response. Frenetic needs to frequently refer to the API description to see
96
+ what is possible. This will result in lots of HTTP requests if you don't tell
97
+ it how long to wait before checking again.
97
98
 
99
+ If the API does return Cache-Control headers, Frenetic will always cache this
100
+ response regardless of which caching middleware you have configured or even if
101
+ you have caching disabled.
102
+
103
+ If you have no control over the API, refer to the
104
+ [Default Root Cache Age][root_cache] section.
98
105
 
99
106
 
100
107
 
@@ -250,6 +257,17 @@ Frenetic.new( url:url, adapter:Faraday::Adapter::Patron )
250
257
  ```
251
258
 
252
259
 
260
+ #### Default Root Cache Age
261
+
262
+ If you have no control over the API, you can explicitly tell Frenetic how long
263
+ to cache the API description for:
264
+
265
+ ```ruby
266
+ Frenetic.new( url:url, default_root_cache_age:1.hour )
267
+ ```
268
+
269
+
270
+
253
271
  #### Faraday Middleware
254
272
 
255
273
  Frenetic will yield its internal Faraday connection during initialization:
@@ -341,6 +359,7 @@ ideas on how to support other Hypermedia formats like [Collection+JSON][coll_jso
341
359
  [spire.io]: http://api.spire.io/
342
360
  [caching]: #response-caching
343
361
  [faraday]: https://github.com/technoweenie/faraday
362
+ [root_cache]: #default-root-cache-age
344
363
  [adapters]: https://github.com/lostisland/faraday/blob/c26a060acdd9eae356409c2ca79f1c22f8263de9/lib/faraday/adapter.rb#L7-L17
345
364
  [rack_cache]: https://github.com/rtomayko/rack-cache
346
365
  [coll_json]: http://amundsen.com/media-types/collection/
@@ -24,4 +24,5 @@ Gem::Specification.new do |gem|
24
24
  gem.add_development_dependency 'rspec', '~> 2.13.0'
25
25
  gem.add_development_dependency 'rack-cache', '~> 1.2'
26
26
  gem.add_development_dependency 'webmock', '~> 1.11.0'
27
+ gem.add_development_dependency 'timecop', '~> 0.6.1'
27
28
  end
@@ -4,6 +4,7 @@ require 'faraday_middleware'
4
4
 
5
5
  require 'frenetic/version'
6
6
  require 'frenetic/concerns/configurable'
7
+ require 'frenetic/concerns/briefly_memoizable'
7
8
  require 'frenetic/middleware/hal_json'
8
9
  require 'frenetic/resource'
9
10
  require 'frenetic/resource_collection'
@@ -11,6 +12,7 @@ require 'frenetic/resource_collection'
11
12
  class Frenetic
12
13
  extend Forwardable
13
14
  include Configurable
15
+ include BrieflyMemoizable
14
16
 
15
17
  def_delegators :connection, :get, :put, :post, :delete
16
18
 
@@ -39,16 +41,35 @@ class Frenetic
39
41
  end
40
42
  end
41
43
 
42
- # It is highly advised that the server responds with some cache headers and
43
- # the API Client is configured to use a Faraday caching strategy
44
+ # Since Frenetic needs to frequently refer to the API design, the result of
45
+ # this method is essentially cached, regardless of what caching middleware it
46
+ # is configured with.
47
+ #
48
+ # It fully honors the HTTP Cache-Control headers that are returned by the API.
49
+ #
50
+ # If no Cache-Control header is returned, then the results are not memoized.
44
51
  def description
45
52
  if response = get( config.url.to_s ) and response.success?
53
+ @description_age = cache_control_age( response.headers )
54
+
46
55
  response.body
47
56
  end
48
57
  end
58
+ briefly_memoize :description
49
59
 
50
60
  def schema
51
61
  description['_embedded']['schema']
52
62
  end
53
63
 
64
+ private
65
+
66
+ def cache_control_age( headers )
67
+ if cache_age = headers['Cache-Control']
68
+ age = cache_age.match(%r{max-age=(?<max_age>\d+)})[:max_age]
69
+
70
+ Time.now + age.to_i
71
+ else
72
+ config.default_root_cache_age
73
+ end
74
+ end
54
75
  end
@@ -0,0 +1,30 @@
1
+ require 'active_support/concern'
2
+
3
+ # Memoizes method calls, but only for a specific period of time.
4
+ # Useful for supporting HTTP Cache-Control without an external caching layer
5
+ # like Rack::Cache
6
+ module BrieflyMemoizable
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ def briefly_memoize( symbol )
11
+ original_method = "_unmemoized_#{symbol}".to_sym
12
+ memoized_ivar = "@#{symbol}"
13
+ age_ivar = "@#{symbol}_age"
14
+
15
+ class_eval <<-EOS
16
+ if method_defined?(:#{original_method}) # if method_defined?(:_unmemoized_mime_type)
17
+ raise "Already memoized #{symbol}" # raise "Already memoized mime_type"
18
+ end # end
19
+ alias #{original_method} #{symbol} # alias _unmemoized_mime_type mime_type
20
+
21
+ def #{symbol}(*args) # def mime_type(*args)
22
+ #{memoized_ivar} = nil if #{age_ivar} && Time.now > #{age_ivar} # @mime_type = nil if @mime_type_age && Time.now > @mime_type_age
23
+ #
24
+ #{memoized_ivar} ||= #{original_method}(*args) # @mime_type ||= _unmemoized_mime_type(*args)
25
+ end # end
26
+ EOS
27
+ end
28
+ end
29
+
30
+ end
@@ -28,6 +28,7 @@ class Frenetic
28
28
  adapter: adapter,
29
29
  api_token: api_token,
30
30
  cache: cache,
31
+ default_root_cache_age: default_root_cache_age,
31
32
  headers: headers,
32
33
  password: password,
33
34
  url: url,
@@ -47,6 +48,10 @@ class Frenetic
47
48
  end
48
49
  end
49
50
 
51
+ def default_root_cache_age
52
+ @_cfg[:default_root_cache_age]
53
+ end
54
+
50
55
  def headers
51
56
  @@defaults[:headers].merge( @_cfg[:headers] || {} )
52
57
  end
@@ -1,3 +1,3 @@
1
1
  class Frenetic
2
- VERSION = '0.0.20.alpha.1'
2
+ VERSION = '0.0.20.alpha.2'
3
3
  end
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe BrieflyMemoizable do
4
+ let(:my_class) { Class.new }
5
+
6
+ before do
7
+ stub_const 'MyClass', my_class
8
+
9
+ MyClass.send :include, described_class
10
+ MyClass.class_eval do
11
+ def fetch
12
+ @fetch_age = Time.now + 3600
13
+
14
+ external_call
15
+ end
16
+ briefly_memoize :fetch
17
+
18
+ def external_call
19
+ true
20
+ end
21
+ end
22
+ end
23
+
24
+ let(:instance) { MyClass.new }
25
+
26
+ describe '.briefly_memoize' do
27
+ context 'for an expensive method' do
28
+ before do
29
+ Timecop.freeze
30
+
31
+ instance.fetch
32
+ end
33
+
34
+ context 'which is called again outside the memoize window' do
35
+ before do
36
+ Timecop.travel Time.now + 5400
37
+ end
38
+
39
+ it 'should be called' do
40
+ instance.should_receive(:external_call).once.and_call_original
41
+
42
+ instance.fetch
43
+ end
44
+ end
45
+
46
+ context 'which is called again within the memoize window' do
47
+ before do
48
+ Timecop.travel Time.now + 1800
49
+ end
50
+
51
+ it 'should not be called' do
52
+ instance.should_receive(:external_call).never.and_call_original
53
+
54
+ instance.fetch
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -15,6 +15,7 @@ describe Frenetic::Configuration do
15
15
  it { should include :adapter }
16
16
  it { should include :api_token }
17
17
  it { should include :cache }
18
+ it { should include :default_root_cache_age }
18
19
  it { should include :headers }
19
20
  it { should include :password }
20
21
  it { should include :url }
@@ -47,6 +48,16 @@ describe Frenetic::Configuration do
47
48
  end
48
49
  end
49
50
 
51
+ describe '#default_root_cache_age' do
52
+ let(:cfg) do
53
+ { default_root_cache_age:3600 }
54
+ end
55
+
56
+ subject { instance.default_root_cache_age }
57
+
58
+ it { should == 3600 }
59
+ end
60
+
50
61
  describe '#headers' do
51
62
  let(:cfg) do
52
63
  { headers:{ accept:'MIME', x_foo:'BAR' } }
@@ -15,7 +15,12 @@ class HttpStubs
15
15
  end
16
16
 
17
17
  def response( params = {} )
18
- defaults.merge( params ).tap do |p|
18
+ defs = defaults.dup
19
+ headers = params.delete :headers
20
+
21
+ defs[:headers].merge! headers || {}
22
+
23
+ defs.merge( params ).tap do |p|
19
24
  p[:body] = p[:body].to_json
20
25
  end
21
26
  end
@@ -36,7 +41,7 @@ class HttpStubs
36
41
 
37
42
  def api_description
38
43
  @rspec.stub_request( :any, 'example.com/api' )
39
- .to_return response( body:schema )
44
+ .to_return response( body:schema, headers:{ 'Cache-Control' => 'max-age=3600, public' } )
40
45
  end
41
46
 
42
47
  def unknown_resource
@@ -0,0 +1 @@
1
+ require 'timecop'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: frenetic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.20.alpha.1
4
+ version: 0.0.20.alpha.2
5
5
  prerelease: 7
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-11 00:00:00.000000000 Z
12
+ date: 2013-05-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday
@@ -139,6 +139,22 @@ dependencies:
139
139
  - - ~>
140
140
  - !ruby/object:Gem::Version
141
141
  version: 1.11.0
142
+ - !ruby/object:Gem::Dependency
143
+ name: timecop
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ~>
148
+ - !ruby/object:Gem::Version
149
+ version: 0.6.1
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ~>
156
+ - !ruby/object:Gem::Version
157
+ version: 0.6.1
142
158
  description: An opinionated Ruby-based Hypermedia API client.
143
159
  email:
144
160
  - dlindahl@customink.com
@@ -156,6 +172,7 @@ files:
156
172
  - Rakefile
157
173
  - frenetic.gemspec
158
174
  - lib/frenetic.rb
175
+ - lib/frenetic/concerns/briefly_memoizable.rb
159
176
  - lib/frenetic/concerns/collection_rest_methods.rb
160
177
  - lib/frenetic/concerns/configurable.rb
161
178
  - lib/frenetic/concerns/hal_linked.rb
@@ -166,6 +183,7 @@ files:
166
183
  - lib/frenetic/resource.rb
167
184
  - lib/frenetic/resource_collection.rb
168
185
  - lib/frenetic/version.rb
186
+ - spec/concerns/breifly_memoizable_spec.rb
169
187
  - spec/concerns/configurable_spec.rb
170
188
  - spec/concerns/hal_linked_spec.rb
171
189
  - spec/concerns/member_rest_methods_spec.rb
@@ -178,6 +196,7 @@ files:
178
196
  - spec/resource_spec.rb
179
197
  - spec/spec_helper.rb
180
198
  - spec/support/rspec.rb
199
+ - spec/support/timecop.rb
181
200
  - spec/support/webmock.rb
182
201
  homepage: http://dlindahl.github.com/frenetic/
183
202
  licenses: []
@@ -193,7 +212,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
193
212
  version: '0'
194
213
  segments:
195
214
  - 0
196
- hash: 3155215012815263773
215
+ hash: -863323542650757064
197
216
  required_rubygems_version: !ruby/object:Gem::Requirement
198
217
  none: false
199
218
  requirements:
@@ -208,6 +227,7 @@ specification_version: 3
208
227
  summary: Here lies a Ruby-based Hypermedia API client that expects HAL+JSON and makes
209
228
  a lot of assumptions about your API.
210
229
  test_files:
230
+ - spec/concerns/breifly_memoizable_spec.rb
211
231
  - spec/concerns/configurable_spec.rb
212
232
  - spec/concerns/hal_linked_spec.rb
213
233
  - spec/concerns/member_rest_methods_spec.rb
@@ -220,4 +240,5 @@ test_files:
220
240
  - spec/resource_spec.rb
221
241
  - spec/spec_helper.rb
222
242
  - spec/support/rspec.rb
243
+ - spec/support/timecop.rb
223
244
  - spec/support/webmock.rb