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 +4 -4
- data/.github/workflows/build.yml +1 -4
- data/CHANGELOG.md +12 -0
- data/docs/associations.md +34 -15
- data/flexirest.gemspec +1 -0
- data/lib/flexirest/caching.rb +8 -1
- data/lib/flexirest/request.rb +4 -4
- data/lib/flexirest/version.rb +1 -1
- data/spec/lib/caching_spec.rb +55 -26
- data/spec/lib/lazy_association_loader_spec.rb +27 -0
- data/spec/spec_helper.rb +1 -0
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '069a842d7b7b6db2e2aa7cff28191a02d70e0ebe3bc6f49aadf569b8006b7cc4'
|
4
|
+
data.tar.gz: 9db8212da8ed8698bc4a1d48137fe2e0e9ce8d75c94366d72fe1f78d986855e0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb73957659a29bb35ab8f08b9127e32f36e733e9a37830c09e42f221774531c18edbbb4767bccd1fcc4713a938fb9c195daa4863af39c128b312d582b6245fc2
|
7
|
+
data.tar.gz: b9e91c14da366548a6a618eae2d634ad26cf60e1ed20d957b577bb4e1e777c32eea594ebc2a37990f61eeb59c41e34912ed591477f0a80005bfc7435dac7aa3d
|
data/.github/workflows/build.yml
CHANGED
@@ -24,10 +24,7 @@ jobs:
|
|
24
24
|
steps:
|
25
25
|
- uses: actions/checkout@v2
|
26
26
|
- name: Set up Ruby
|
27
|
-
|
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
|
-
|
89
|
+
If the call for an attribute should
|
90
90
|
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
-
|
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 =>
|
103
|
+
get :find, "/people/:id", :lazy => { books: Book }
|
106
104
|
end
|
107
105
|
```
|
108
106
|
|
109
|
-
|
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
|
-
#
|
116
|
-
#
|
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"
|
data/lib/flexirest/caching.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/flexirest/request.rb
CHANGED
@@ -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
|
-
|
716
|
+
parent_name = name
|
717
717
|
object = @method[:options][:has_many][name].new
|
718
718
|
elsif @method[:options][:has_one][name]
|
719
|
-
|
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
|
-
|
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
|
|
data/lib/flexirest/version.rb
CHANGED
data/spec/lib/caching_spec.rb
CHANGED
@@ -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
|
-
|
88
|
-
def fetch(key, &block) ; end
|
82
|
+
class CachingExampleCacheStore2 < ActiveSupport::Cache::MemoryStore
|
83
|
+
undef_method :read
|
89
84
|
end
|
90
|
-
class CachingExampleCacheStore3
|
91
|
-
|
92
|
-
def fetch(key, &block) ; end
|
85
|
+
class CachingExampleCacheStore3 < ActiveSupport::Cache::MemoryStore
|
86
|
+
undef_method :write
|
93
87
|
end
|
94
|
-
class CachingExampleCacheStore4
|
95
|
-
|
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),
|
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
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.
|
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-
|
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.
|
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
|