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

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.
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