flexirest 1.11.0 → 1.11.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3c99c9dcb4b2b2d6b4992a53d138877f251f0810bb94dc11faf9d9e1b89f9cb
4
- data.tar.gz: 22554cafc10d12337c1511828ab27471dbadf36de4e211c5383eae96c4fb4c34
3
+ metadata.gz: '069a842d7b7b6db2e2aa7cff28191a02d70e0ebe3bc6f49aadf569b8006b7cc4'
4
+ data.tar.gz: 9db8212da8ed8698bc4a1d48137fe2e0e9ce8d75c94366d72fe1f78d986855e0
5
5
  SHA512:
6
- metadata.gz: 3cf855f9fbc7d4f213a19840e11cfa9b816de9b1a0750726538f288f559ec150c7a05a4ae8b63297e8aedcf8002103e2e4ae275cb497793144c83aebc7f7577e
7
- data.tar.gz: e1a5b90ace51f341a340e5ed32f5c8afc30f2784a38cf7281c621157a8b1233da9b81180ed6a5dc0694f4ac72933793b9e47dcf01d9389583b02bdd7dc3aefbd
6
+ metadata.gz: fb73957659a29bb35ab8f08b9127e32f36e733e9a37830c09e42f221774531c18edbbb4767bccd1fcc4713a938fb9c195daa4863af39c128b312d582b6245fc2
7
+ data.tar.gz: b9e91c14da366548a6a618eae2d634ad26cf60e1ed20d957b577bb4e1e777c32eea594ebc2a37990f61eeb59c41e34912ed591477f0a80005bfc7435dac7aa3d
@@ -24,10 +24,7 @@ jobs:
24
24
  steps:
25
25
  - uses: actions/checkout@v2
26
26
  - name: Set up Ruby
27
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
28
- # change this to (see https://github.com/ruby/setup-ruby#versioning):
29
- # uses: ruby/setup-ruby@v1
30
- uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
27
+ uses: ruby/setup-ruby@v1
31
28
  with:
32
29
  ruby-version: ${{ matrix.ruby-version }}
33
30
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.11.2
4
+
5
+ Bugfix:
6
+
7
+ - When a model had multiple lazy loaders specified, they would all have their object class set to the first object class that was found when iterating over them. (thanks to Joshua Samberg for the PR)
8
+
9
+ ## 1.11.1
10
+
11
+ Enhancement:
12
+
13
+ - Add automatic expiry of cached responses based on the Expires header if set (thanks to Romain Gisiger for the issue and PR)
14
+
3
15
  ## 1.11.0
4
16
 
5
17
  Major change:
data/docs/associations.md CHANGED
@@ -86,37 +86,56 @@ When the `:array` option includes an attribute, it is assumed the values were re
86
86
 
87
87
  ## Type 2 - Lazy Loading From Other URLs
88
88
 
89
- When mapping the method, passing a list of attributes will cause any requests for those attributes to mapped to the URLs given in their responses. The response for the attribute may be one of the following:
89
+ If the call for an attribute should
90
90
 
91
- ```ruby
92
- "attribute" : "URL"
93
- "attribute" : ["URL", "URL"]
94
- "attribute" : { "url" : "URL"}
95
- "attribute" : { "href" : "URL"}
96
- "attribute" : { "something" : "URL"}
97
- ```
98
-
99
- The difference between the last 3 examples is that a key of `url` or `href` signifies it's a single object that is lazy loaded from the value specified. Any other keys assume that it's a nested set of URLs (like in the array situation, but accessible via the keys - e.g. object.attribute.something in the above example).
91
+ + Use the value held within the specified attribute as a URL(s) from which to load the associated resource(s)
92
+ + **THEN**, create an instance of your API object from the result
93
+ + **THEN**, call subsequent chained methods on that instance
100
94
 
101
- It is required that the URL is a complete URL including a protocol starting with "http". To configure this use code like:
95
+ you can specify this when mapping the method by passing attribtues to the `:lazy` option.
102
96
 
103
97
  ```ruby
98
+ class Book < Flexirest::Base
99
+ get :find, "/books/:name"
100
+ end
101
+
104
102
  class Person < Flexirest::Base
105
- get :find, "/people/:id", :lazy => [:orders, :refunds]
103
+ get :find, "/people/:id", :lazy => { books: Book }
106
104
  end
107
105
  ```
108
106
 
109
- And use it like this:
107
+ Use it like this:
110
108
 
111
109
  ```ruby
112
110
  # Makes a call to /people/1
113
111
  @person = Person.find(1)
114
112
 
115
- # Makes a call to the first URL found in the "books":[...] array in the article response
116
- # only makes the HTTP request when first used though
113
+ # Lazily makes a call to the first URL found in the "books":[...] array in the Person.find response
114
+ # - Only makes the HTTP request when first used
115
+ # - Instantiates a Book object from the response and then accesses its "name" property
117
116
  @person.books.first.name
118
117
  ```
119
118
 
119
+ ### URLs in API responses
120
+ To provide a URL(s) for lazy loading an attribute, the API response may contain one of the following for the attribute (**`book`** or **`books`** is the attribute in all examples below):
121
+
122
+ ```ruby
123
+ # all of the following will lazy load a single object from the specified URL
124
+ "book" : "https://example.com/books/1"
125
+ # or
126
+ "book" : { "url" : "https://example.com/books/1"}
127
+ # or
128
+ "book" : { "href" : "https://example.com/books/1"}
129
+
130
+ # books will be an array whose elements will be lazy loaded one-by-one, from the URL in the corresponding array position, whenever each element is first accessed
131
+ "books" : ["https://example.com/books/1", "https://example.com/books/2"]
132
+
133
+ # book will be an object where the values will be lazy loaded one-by-one, from the URL in the corresponding key, whenever each key is first accessed (e.g. the first time object.book.author is accessed)
134
+ "book" : { "author" : "https://example.com/author/1"}
135
+ ```
136
+
137
+ It is required that each URL is a complete URL including a protocol starting with `http`.
138
+
120
139
  ## Type 3 - HAL Auto-loaded Resources
121
140
 
122
141
  You don't need to define lazy attributes if they are defined using [HAL](http://stateless.co/hal_specification.html) (with an optional embedded representation). If your resource has an `_links` item (and optionally an `_embedded` item) then it will automatically treat the linked resources (with the `_embedded` cache) as if they were defined using `:lazy` as per type 2 above.
data/flexirest.gemspec CHANGED
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.add_development_dependency 'faraday-typhoeus'
37
37
  spec.add_development_dependency 'activemodel'
38
38
  spec.add_development_dependency 'rest-client'
39
+ spec.add_development_dependency 'timecop'
39
40
 
40
41
  spec.add_runtime_dependency "mime-types"
41
42
  spec.add_runtime_dependency "multi_json"
@@ -74,7 +74,14 @@ module Flexirest
74
74
  cached_response.etag = "#{headers[:etag]}" if headers[:etag]
75
75
  cached_response.expires = Time.parse(headers[:expires]) rescue nil if headers[:expires]
76
76
  if cached_response.etag.present? || cached_response.expires
77
- cache_store.write(key, Marshal.dump(cached_response), {})
77
+ options = {}
78
+ if cached_response.expires.present?
79
+ exp_in_seconds = cached_response.expires.utc - Time.now.utc
80
+ return unless exp_in_seconds.positive?
81
+
82
+ options[:expires_in] = exp_in_seconds
83
+ end
84
+ cache_store.write(key, Marshal.dump(cached_response), options)
78
85
  end
79
86
  end
80
87
  end
@@ -713,12 +713,13 @@ module Flexirest
713
713
  @method[:options][:has_many] ||= {}
714
714
  name = name.to_sym rescue nil
715
715
  if @method[:options][:has_many][name]
716
- overridden_name = name
716
+ parent_name = name
717
717
  object = @method[:options][:has_many][name].new
718
718
  elsif @method[:options][:has_one][name]
719
- overridden_name = name
719
+ parent_name = name
720
720
  object = @method[:options][:has_one][name].new
721
721
  else
722
+ parent_name = nil
722
723
  object = create_object_instance
723
724
  end
724
725
 
@@ -735,8 +736,7 @@ module Flexirest
735
736
  else
736
737
  k = k.to_sym
737
738
  end
738
- overridden_name = select_name(k, overridden_name)
739
- set_corresponding_value(v, k, object, overridden_name)
739
+ set_corresponding_value(v, k, object, select_name(k, parent_name))
740
740
  end
741
741
  object.clean! unless object_is_class?
742
742
 
@@ -1,3 +1,3 @@
1
1
  module Flexirest
2
- VERSION = "1.11.0"
2
+ VERSION = "1.11.2"
3
3
  end
@@ -72,28 +72,21 @@ describe Flexirest::Caching do
72
72
  end
73
73
 
74
74
  it "should use a custom cache store if a valid one is manually set" do
75
- class CachingExampleCacheStore1
76
- def read(key) ; end
77
- def write(key, value, options={}) ; end
78
- def fetch(key, &block) ; end
79
- end
75
+ class CachingExampleCacheStore1 < ActiveSupport::Cache::MemoryStore; end
80
76
  cache_store = CachingExampleCacheStore1.new
81
77
  Flexirest::Base.cache_store = cache_store
82
78
  expect(Flexirest::Base.cache_store).to eq(cache_store)
83
79
  end
84
80
 
85
81
  it "should error if you try to use a custom cache store that doesn't match the required interface" do
86
- class CachingExampleCacheStore2
87
- def write(key, value, options={}) ; end
88
- def fetch(key, &block) ; end
82
+ class CachingExampleCacheStore2 < ActiveSupport::Cache::MemoryStore
83
+ undef_method :read
89
84
  end
90
- class CachingExampleCacheStore3
91
- def read(key) ; end
92
- def fetch(key, &block) ; end
85
+ class CachingExampleCacheStore3 < ActiveSupport::Cache::MemoryStore
86
+ undef_method :write
93
87
  end
94
- class CachingExampleCacheStore4
95
- def read(key) ; end
96
- def write(key, value, options={}) ; end
88
+ class CachingExampleCacheStore4 < ActiveSupport::Cache::MemoryStore
89
+ undef_method :fetch
97
90
  end
98
91
 
99
92
  expect{ Flexirest::Base.cache_store = CachingExampleCacheStore2.new }.to raise_error(Flexirest::InvalidCacheStoreException)
@@ -109,11 +102,7 @@ describe Flexirest::Caching do
109
102
  context "Reading/writing to the cache" do
110
103
  before :each do
111
104
  Object.send(:remove_const, :CachingExampleCacheStore5) if defined?(CachingExampleCacheStore5)
112
- class CachingExampleCacheStore5
113
- def read(key) ; end
114
- def write(key, value, options={}) ; end
115
- def fetch(key, &block) ; end
116
- end
105
+ class CachingExampleCacheStore5 < ActiveSupport::Cache::MemoryStore; end
117
106
 
118
107
  class Person < Flexirest::Base
119
108
  perform_caching true
@@ -122,7 +111,7 @@ describe Flexirest::Caching do
122
111
  put :save_all, "/"
123
112
  end
124
113
 
125
- Person.cache_store = CachingExampleCacheStore5.new
114
+ Person.cache_store = CachingExampleCacheStore5.new({ expires_in: 1.day.to_i }) # default cache expiration
126
115
 
127
116
  @etag = "6527914a91e0c5769f6de281f25bd891"
128
117
  @cached_object = Person.new(first_name:"Johnny")
@@ -173,7 +162,7 @@ describe Flexirest::Caching do
173
162
  expect(result.first_name).to eq new_name
174
163
  end
175
164
 
176
- it "should read from the cache store, and not call the server if there's a hard expiry" do
165
+ it "should read from the cache store, and not call the server if there's a hard expiry not passed yet" do
177
166
  cached_response = Flexirest::CachedResponse.new(
178
167
  status:200,
179
168
  result:@cached_object,
@@ -184,6 +173,18 @@ describe Flexirest::Caching do
184
173
  expect(ret.first_name).to eq("Johnny")
185
174
  end
186
175
 
176
+ it "should not read from the cache store, and call the server if there's a hard expiry already passed" do
177
+ cached_response = Flexirest::CachedResponse.new(
178
+ status:200,
179
+ result:@cached_object,
180
+ expires:Time.now + 30)
181
+ Timecop.travel(Time.now + 60)
182
+ expect_any_instance_of(CachingExampleCacheStore5).to receive(:read).once.with("Person:/").and_return(nil)
183
+ expect_any_instance_of(Flexirest::Connection).to receive(:get).with("/", an_instance_of(Hash)).and_return(::FaradayResponseMock.new(OpenStruct.new(status:200, body:"{\"result\":true}", response_headers:{})))
184
+ ret = Person.all
185
+ Timecop.return
186
+ end
187
+
187
188
  it "cache read objects shouldn't be marked as changed" do
188
189
  cached_response = Flexirest::CachedResponse.new(
189
190
  status:200,
@@ -208,7 +209,7 @@ describe Flexirest::Caching do
208
209
  expect(ret.first_name).to eq("Johnny")
209
210
  end
210
211
 
211
- it "should restore a result iterator from the cache store, if there's a hard expiry" do
212
+ it "should restore a result iterator from the cache store, if there's a hard expiry not passed yet" do
212
213
  class CachingExample3 < Flexirest::Base ; end
213
214
  object = Flexirest::ResultIterator.new(double(status: 200))
214
215
  object << CachingExample3.new(first_name:"Johnny")
@@ -226,6 +227,26 @@ describe Flexirest::Caching do
226
227
  expect(ret._status).to eq(200)
227
228
  end
228
229
 
230
+ it "should not restore a result iterator from the cache store, if there's a hard expiry already passed" do
231
+ class CachingExample3 < Flexirest::Base ; end
232
+ object = Flexirest::ResultIterator.new(double(status: 200))
233
+ object << CachingExample3.new(first_name:"Johnny")
234
+ object << CachingExample3.new(first_name:"Billy")
235
+ etag = "6527914a91e0c5769f6de281f25bd891"
236
+ cached_response = Flexirest::CachedResponse.new(
237
+ status:200,
238
+ result:object,
239
+ etag:etag,
240
+ expires:Time.now + 30)
241
+ Timecop.travel(Time.now + 60)
242
+ expect_any_instance_of(CachingExampleCacheStore5).to receive(:read).once.with("Person:/").and_return(nil)
243
+ expect_any_instance_of(Flexirest::Connection).to receive(:get).with("/", an_instance_of(Hash)).and_return(::FaradayResponseMock.new(OpenStruct.new(status:200, body:"[{\"first_name\":\"Billy\"}]", response_headers:{})))
244
+ ret = Person.all
245
+ expect(ret.first.first_name).to eq("Billy")
246
+ expect(ret._status).to eq(200)
247
+ Timecop.return
248
+ end
249
+
229
250
  it "should not write the response to the cache unless it has caching headers" do
230
251
  expect_any_instance_of(CachingExampleCacheStore5).to receive(:read).once.with("Person:/").and_return(nil)
231
252
  expect_any_instance_of(CachingExampleCacheStore5).not_to receive(:write)
@@ -233,9 +254,9 @@ describe Flexirest::Caching do
233
254
  Person.all
234
255
  end
235
256
 
236
- it "should write the response to the cache if there's an etag" do
257
+ it "should write the response to the cache without expires_in option if there's an etag" do
237
258
  expect_any_instance_of(CachingExampleCacheStore5).to receive(:read).once.with("Person:/").and_return(nil)
238
- expect_any_instance_of(CachingExampleCacheStore5).to receive(:write).once.with("Person:/", an_instance_of(String), {})
259
+ expect_any_instance_of(CachingExampleCacheStore5).to receive(:write).once.with("Person:/", an_instance_of(String), hash_excluding(:expires_in))
239
260
  expect_any_instance_of(Flexirest::Connection).to receive(:get).with("/", an_instance_of(Hash)).and_return(::FaradayResponseMock.new(OpenStruct.new(status:200, body:"{\"result\":true}", response_headers:{etag:"1234567890"})))
240
261
  Person.perform_caching true
241
262
  Person.all
@@ -264,14 +285,22 @@ describe Flexirest::Caching do
264
285
  end
265
286
  end
266
287
 
267
- it "should write the response to the cache if there's a hard expiry" do
288
+ it "should write the response to the cache with expires_in option if there's a hard expiry in the future" do
268
289
  expect_any_instance_of(CachingExampleCacheStore5).to receive(:read).once.with("Person:/").and_return(nil)
269
- expect_any_instance_of(CachingExampleCacheStore5).to receive(:write).once.with("Person:/", an_instance_of(String), an_instance_of(Hash))
290
+ expect_any_instance_of(CachingExampleCacheStore5).to receive(:write).once.with("Person:/", an_instance_of(String), hash_including(:expires_in))
270
291
  expect_any_instance_of(Flexirest::Connection).to receive(:get).with("/", an_instance_of(Hash)).and_return(::FaradayResponseMock.new(OpenStruct.new(status:200, body:"{\"result\":true}", response_headers:{expires:(Time.now + 30).rfc822})))
271
292
  Person.perform_caching = true
272
293
  Person.all
273
294
  end
274
295
 
296
+ it "should not write the response to the cache if there's a hard expiry in the past" do
297
+ expect_any_instance_of(CachingExampleCacheStore5).to receive(:read).once.with("Person:/").and_return(nil)
298
+ expect_any_instance_of(CachingExampleCacheStore5).not_to receive(:write)
299
+ expect_any_instance_of(Flexirest::Connection).to receive(:get).with("/", an_instance_of(Hash)).and_return(::FaradayResponseMock.new(OpenStruct.new(status:200, body:"{\"result\":true}", response_headers:{expires:(Time.now - 10).rfc822})))
300
+ Person.perform_caching = true
301
+ Person.all
302
+ end
303
+
275
304
  it "should not write the response to the cache if there's an invalid expiry" do
276
305
  expect_any_instance_of(CachingExampleCacheStore5).to receive(:read).once.with("Person:/").and_return(nil)
277
306
  expect_any_instance_of(CachingExampleCacheStore5).to_not receive(:write).once.with("Person:/", an_instance_of(String), an_instance_of(Hash))
@@ -12,6 +12,24 @@ class YearExample < Flexirest::Base
12
12
  get :find, "/year/:id", lazy: { months: MonthExample }, fake: "{\"months\": [\"http://www.example.com/months/1\"] }"
13
13
  end
14
14
 
15
+ class CenturyExample < Flexirest::Base
16
+ base_url "http://www.example.com"
17
+
18
+ get :find, "century/:id"
19
+ end
20
+
21
+ class DecadeExample < Flexirest::Base
22
+ base_url "http://www.example.com"
23
+
24
+ get :find, "/decade/:id", lazy: { years: YearExample, century: CenturyExample }, fake: %Q{
25
+ {
26
+ "years": ["http://www.example.com/years/1"],
27
+ "century": "http://www.example.com/century/1"
28
+ }
29
+ }
30
+ end
31
+
32
+
15
33
  describe Flexirest::LazyAssociationLoader do
16
34
  let(:url1) { "http://www.example.com/some/url" }
17
35
  let(:url2) { "http://www.example.com/some/other" }
@@ -132,4 +150,13 @@ describe Flexirest::LazyAssociationLoader do
132
150
  association = YearExample.find(1)
133
151
  expect(association.months.instance_variable_get('@request').instance_variable_get('@object').class).to eq(MonthExample)
134
152
  end
153
+
154
+ context "has multiple associations" do
155
+ it "should correctly map each association name to the class specified for that association in the 'lazy' declaration " do
156
+ association = DecadeExample.find(1)
157
+ expect(association.years.instance_variable_get('@request').instance_variable_get('@object').class).to eq(YearExample)
158
+ expect(association.century.instance_variable_get('@request').instance_variable_get('@object').class).to eq(CenturyExample)
159
+ end
160
+ end
161
+
135
162
  end
data/spec/spec_helper.rb CHANGED
@@ -3,6 +3,7 @@ require 'simplecov'
3
3
  require 'flexirest'
4
4
  require "ostruct"
5
5
  require 'webmock/rspec'
6
+ require 'timecop'
6
7
 
7
8
  if ENV["JENKINS"]
8
9
  require 'simplecov-rcov'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flexirest
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.11.0
4
+ version: 1.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Jeffries
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-03 00:00:00.000000000 Z
11
+ date: 2023-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: timecop
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
181
195
  - !ruby/object:Gem::Dependency
182
196
  name: mime-types
183
197
  requirement: !ruby/object:Gem::Requirement
@@ -393,7 +407,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
393
407
  - !ruby/object:Gem::Version
394
408
  version: '0'
395
409
  requirements: []
396
- rubygems_version: 3.3.7
410
+ rubygems_version: 3.4.6
397
411
  signing_key:
398
412
  specification_version: 4
399
413
  summary: This gem is for accessing REST services in a flexible way. ActiveResource