nexus_mods 0.4.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79e4d81970dd1549db43105a2a155b6b275692df4f06827e2234ab02fce6276b
4
- data.tar.gz: 4d96df8a39784c8a2eb6fb9bd72a247f3cafa2765aa03059dfc661afd9db7a38
3
+ metadata.gz: 758229de2b795e860d8b150e4bbc8a2d516f6e49c2773ac242c0b67472f6c157
4
+ data.tar.gz: 96e38216e7f54b9198c9362bb01b7ea0fdde2205e357a419123b3337c2791ee4
5
5
  SHA512:
6
- metadata.gz: 9c13d188694e09dc06998846791322a4887bbb5163ff634d5e6f813abca187a6a2dc8aeb102e040b43812d6276feb6c1ee59665c5e5e408c6acbff4dca0539b3
7
- data.tar.gz: 3e37eb153f00bb6335efb8e0b377065f4b0613cbef46066a1a5d7095b1e1767b1786d59f28681298e75be49e1cfeb739ea6ce2f7b13fefc6b28362b27d7536b5
6
+ metadata.gz: 8c2fa3106f59e59472bcb39d107d28bba9ce394e9c7d2783d5b60e0074ea7070bfd175aaa1dd543397b057aaabd7bd0778f299609acbf30a706870f4460c95ab
7
+ data.tar.gz: f2f6994f254288a5c8b7ba806a3f0b9b18a044aacdf9b7edd381b629a6e97c9d49f881ba566437beb4739cf32a2a0b11575fd71f574210e62763fb5fcc86b8cb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # [v0.5.0](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.4.0...v0.5.0) (2023-04-10 17:05:11)
2
+
3
+ ### Features
4
+
5
+ * [[Feature] Add clear_cache parameters to invalidate the API cache at will](https://github.com/Muriel-Salvan/nexus_mods/commit/44958e35eae20ef818e3d4735cc3bd3008cfc5df)
6
+
1
7
  # [v0.4.0](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.3.0...v0.4.0) (2023-04-10 10:15:41)
2
8
 
3
9
  ### Features
@@ -63,9 +63,65 @@ class NexusMods
63
63
  # Parameters::
64
64
  # * *path* (String): API path to contact (from v1/ and without .json)
65
65
  # * *verb* (Symbol): Verb to be used (:get, :post...) [default: :get]
66
+ # * *clear_cache* (Boolean): Should we clear the API cache for this resource? [default: false]
66
67
  # Result::
67
68
  # * Object: The JSON response
68
- def api(path, verb: :get)
69
+ def api(path, verb: :get, clear_cache: false)
70
+ clear_cached_api_cache(path, verb:) if clear_cache
71
+ cached_api(path, verb:)
72
+ end
73
+
74
+ # Send an HTTP request to the API and get back the HTTP response
75
+ #
76
+ # Parameters::
77
+ # * *path* (String): API path to contact (from v1/ and without .json)
78
+ # * *verb* (Symbol): Verb to be used (:get, :post...) [default: :get]
79
+ # Result::
80
+ # * Faraday::Response: The HTTP response
81
+ def http(path, verb: :get)
82
+ @http_client.send(verb) do |req|
83
+ req.url api_uri(path)
84
+ req.headers['apikey'] = @api_key
85
+ req.headers['User-Agent'] = "nexus_mods (#{RUBY_PLATFORM}) Ruby/#{RUBY_VERSION}"
86
+ end
87
+ end
88
+
89
+ # Load the API cache if a file was given to this client
90
+ def load_api_cache
91
+ Cacheable.cache_adapter.load(@api_cache_file) if @api_cache_file && File.exist?(@api_cache_file)
92
+ end
93
+
94
+ # Save the API cache if a file was given to this client
95
+ def save_api_cache
96
+ return unless @api_cache_file
97
+
98
+ FileUtils.mkdir_p(File.dirname(@api_cache_file))
99
+ Cacheable.cache_adapter.save(@api_cache_file)
100
+ end
101
+
102
+ # Some attributes exposed for the cacheable feature to work
103
+ attr_reader :api_cache_expiry
104
+
105
+ private
106
+
107
+ class << self
108
+
109
+ # ApiClient: The API client to be used by the cacheable adapter (singleton pattern)
110
+ attr_accessor :api_client
111
+
112
+ end
113
+
114
+ @api_client = nil
115
+
116
+ # Send an HTTP request to the API and get back the answer as a JSON.
117
+ # Use caching.
118
+ #
119
+ # Parameters::
120
+ # * *path* (String): API path to contact (from v1/ and without .json)
121
+ # * *verb* (Symbol): Verb to be used (:get, :post...) [default: :get]
122
+ # Result::
123
+ # * Object: The JSON response
124
+ def cached_api(path, verb: :get)
69
125
  res = http(path, verb:)
70
126
  json = JSON.parse(res.body)
71
127
  uri = api_uri(path)
@@ -93,14 +149,14 @@ class NexusMods
93
149
  json
94
150
  end
95
151
  cacheable_api(
96
- :api,
152
+ :cached_api,
97
153
  expiry_from_key: proc do |key|
98
154
  # Example of keys:
99
- # NexusMods::ApiClient/api/games
100
- # NexusMods::ApiClient/api/games/skyrimspecialedition/mods/2014
101
- # NexusMods::ApiClient/api/games/skyrimspecialedition/mods/2014/files
102
- # NexusMods::ApiClient/api/users/validate
103
- key_components = key.split('/')[2..]
155
+ # NexusMods::ApiClient/cached_api/games/verb:get
156
+ # NexusMods::ApiClient/cached_api/games/skyrimspecialedition/mods/2014/verb:get
157
+ # NexusMods::ApiClient/cached_api/games/skyrimspecialedition/mods/2014/files/verb:get
158
+ # NexusMods::ApiClient/cached_api/users/validate/verb:get
159
+ key_components = key.split('/')[2..-2]
104
160
  case key_components[0]
105
161
  when 'games'
106
162
  if key_components[1].nil?
@@ -132,48 +188,6 @@ class NexusMods
132
188
  end
133
189
  )
134
190
 
135
- # Send an HTTP request to the API and get back the HTTP response
136
- #
137
- # Parameters::
138
- # * *path* (String): API path to contact (from v1/ and without .json)
139
- # * *verb* (Symbol): Verb to be used (:get, :post...) [default: :get]
140
- # Result::
141
- # * Faraday::Response: The HTTP response
142
- def http(path, verb: :get)
143
- @http_client.send(verb) do |req|
144
- req.url api_uri(path)
145
- req.headers['apikey'] = @api_key
146
- req.headers['User-Agent'] = "nexus_mods (#{RUBY_PLATFORM}) Ruby/#{RUBY_VERSION}"
147
- end
148
- end
149
-
150
- # Load the API cache if a file was given to this client
151
- def load_api_cache
152
- Cacheable.cache_adapter.load(@api_cache_file) if @api_cache_file && File.exist?(@api_cache_file)
153
- end
154
-
155
- # Save the API cache if a file was given to this client
156
- def save_api_cache
157
- return unless @api_cache_file
158
-
159
- FileUtils.mkdir_p(File.dirname(@api_cache_file))
160
- Cacheable.cache_adapter.save(@api_cache_file)
161
- end
162
-
163
- # Some attributes exposed for the cacheable feature to work
164
- attr_reader :api_cache_expiry
165
-
166
- private
167
-
168
- class << self
169
-
170
- # ApiClient: The API client to be used by the cacheable adapter (singleton pattern)
171
- attr_accessor :api_client
172
-
173
- end
174
-
175
- @api_client = nil
176
-
177
191
  # Get the real URI to query for a given API path
178
192
  #
179
193
  # Parameters::
@@ -1,5 +1,5 @@
1
1
  class NexusMods
2
2
 
3
- VERSION = '0.4.0'
3
+ VERSION = '0.5.0'
4
4
 
5
5
  end
data/lib/nexus_mods.rb CHANGED
@@ -98,10 +98,12 @@ class NexusMods
98
98
 
99
99
  # Get the list of games
100
100
  #
101
+ # Parameters::
102
+ # * *clear_cache* (Boolean): Should we clear the API cache for this resource? [default: false]
101
103
  # Result::
102
104
  # * Array<Game>: List of games
103
- def games
104
- @api_client.api('games').map do |game_json|
105
+ def games(clear_cache: false)
106
+ @api_client.api('games', clear_cache:).map do |game_json|
105
107
  # First create categories tree
106
108
  # Hash<Integer, [Category, Integer]>: Category and its parent category id, per category id
107
109
  categories = game_json['categories'].to_h do |category_json|
@@ -145,10 +147,11 @@ class NexusMods
145
147
  # Parameters::
146
148
  # * *game_domain_name* (String): Game domain name to query by default [default: @game_domain_name]
147
149
  # * *mod_id* (Integer): The mod ID [default: @mod_id]
150
+ # * *clear_cache* (Boolean): Should we clear the API cache for this resource? [default: false]
148
151
  # Result::
149
152
  # * Mod: Mod information
150
- def mod(game_domain_name: @game_domain_name, mod_id: @mod_id)
151
- mod_json = @api_client.api "games/#{game_domain_name}/mods/#{mod_id}"
153
+ def mod(game_domain_name: @game_domain_name, mod_id: @mod_id, clear_cache: false)
154
+ mod_json = @api_client.api("games/#{game_domain_name}/mods/#{mod_id}", clear_cache:)
152
155
  Api::Mod.new(
153
156
  uid: mod_json['uid'],
154
157
  mod_id: mod_json['mod_id'],
@@ -193,10 +196,11 @@ class NexusMods
193
196
  # Parameters::
194
197
  # * *game_domain_name* (String): Game domain name to query by default [default: @game_domain_name]
195
198
  # * *mod_id* (Integer): The mod ID [default: @mod_id]
199
+ # * *clear_cache* (Boolean): Should we clear the API cache for this resource? [default: false]
196
200
  # Result::
197
201
  # * Array<ModFile>: List of mod's files
198
- def mod_files(game_domain_name: @game_domain_name, mod_id: @mod_id)
199
- @api_client.api("games/#{game_domain_name}/mods/#{mod_id}/files")['files'].map do |file_json|
202
+ def mod_files(game_domain_name: @game_domain_name, mod_id: @mod_id, clear_cache: false)
203
+ @api_client.api("games/#{game_domain_name}/mods/#{mod_id}/files", clear_cache:)['files'].map do |file_json|
200
204
  category_id = FILE_CATEGORIES[file_json['category_id']]
201
205
  raise "Unknown file category: #{file_json['category_id']}" if category_id.nil?
202
206
 
@@ -221,4 +225,9 @@ class NexusMods
221
225
  end
222
226
  end
223
227
 
228
+ # TODO: Check http cache usefulness and either test it or remove it
229
+ # TODO: Check if attr_reader of mod_id and game_id is needed and remove it if not
230
+ # TODO: Find better cache keys
231
+ # TODO: Documentation and examples
232
+
224
233
  end
@@ -59,13 +59,24 @@ module NexusModsTest
59
59
  args[:http_cache_file] = nil unless args.key?(:http_cache_file)
60
60
  args[:api_cache_file] = nil unless args.key?(:api_cache_file)
61
61
  # Redirect any log into a string so that they don't pollute the tests output and they could be asserted.
62
- nexus_mods_logger = StringIO.new
63
- args[:logger] = Logger.new(nexus_mods_logger)
62
+ @nexus_mods_logger = StringIO.new
63
+ args[:logger] = Logger.new(@nexus_mods_logger)
64
64
  @nexus_mods = NexusMods.new(**args)
65
65
  end
66
66
  @nexus_mods
67
67
  end
68
68
 
69
+ # Reset the NexusMods instance.
70
+ # Dump the output if needed for debugging purposes.
71
+ def reset_nexus_mods
72
+ if @nexus_mods && test_debug?
73
+ puts '===== NexusMods output BEGIN ====='
74
+ puts @nexus_mods_logger.string
75
+ puts '===== NexusMods output END ====='
76
+ end
77
+ @nexus_mods = nil
78
+ end
79
+
69
80
  # Expect an HTTP API call to be made, and mock the corresponding HTTP response.
70
81
  # Handle the API key and user agent.
71
82
  #
@@ -150,6 +161,14 @@ module NexusModsTest
150
161
  yield api_cache_file
151
162
  end
152
163
 
164
+ # Are we in test debug mode?
165
+ #
166
+ # Result::
167
+ # * Boolean: Are we in test debug mode?
168
+ def test_debug?
169
+ ENV['TEST_DEBUG'] == '1'
170
+ end
171
+
153
172
  end
154
173
 
155
174
  end
@@ -175,6 +194,11 @@ RSpec.configure do |config|
175
194
  expect(stub).to have_been_made.times(times)
176
195
  end
177
196
  end
197
+ config.around do |example|
198
+ example.call
199
+ ensure
200
+ reset_nexus_mods
201
+ end
178
202
  end
179
203
 
180
204
  # Set a bigger output length for expectation errors, as most error messages include API keys and headers which can be lengthy
@@ -23,6 +23,20 @@ describe NexusMods do
23
23
  expect(nexus_mods.games).to eq(games)
24
24
  end
25
25
 
26
+ it 'does not cache games queries if asked' do
27
+ expect_validate_user
28
+ expect_http_call_to(
29
+ path: '/v1/games.json',
30
+ json: [
31
+ json_game100,
32
+ json_game101
33
+ ],
34
+ times: 2
35
+ )
36
+ games = nexus_mods.games
37
+ expect(nexus_mods.games(clear_cache: true)).to eq(games)
38
+ end
39
+
26
40
  it 'caches mod queries' do
27
41
  expect_validate_user
28
42
  expect_http_call_to(
@@ -33,6 +47,17 @@ describe NexusMods do
33
47
  expect(nexus_mods.mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq(mod)
34
48
  end
35
49
 
50
+ it 'does not cache mod queries if asked' do
51
+ expect_validate_user
52
+ expect_http_call_to(
53
+ path: '/v1/games/skyrimspecialedition/mods/2014.json',
54
+ json: json_complete_mod,
55
+ times: 2
56
+ )
57
+ mod = nexus_mods.mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014)
58
+ expect(nexus_mods.mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014, clear_cache: true)).to eq(mod)
59
+ end
60
+
36
61
  it 'caches mod files queries' do
37
62
  expect_validate_user
38
63
  expect_http_call_to(
@@ -43,6 +68,17 @@ describe NexusMods do
43
68
  expect(nexus_mods.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq(mod_files)
44
69
  end
45
70
 
71
+ it 'does not cache mod files queries if asked' do
72
+ expect_validate_user
73
+ expect_http_call_to(
74
+ path: '/v1/games/skyrimspecialedition/mods/2014/files.json',
75
+ json: { files: [json_mod_file2472, json_mod_file2487] },
76
+ times: 2
77
+ )
78
+ mod_files = nexus_mods.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014)
79
+ expect(nexus_mods.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014, clear_cache: true)).to eq(mod_files)
80
+ end
81
+
46
82
  it 'expires games queries cache' do
47
83
  expect_validate_user
48
84
  expect_http_call_to(
@@ -85,6 +121,25 @@ describe NexusMods do
85
121
  expect(nexus_mods_instance.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq(mod_files)
86
122
  end
87
123
 
124
+ it 'only clears the cache of the wanted resource' do
125
+ expect_validate_user
126
+ expect_http_call_to(
127
+ path: '/v1/games/skyrimspecialedition/mods/2014/files.json',
128
+ json: { files: [json_mod_file2472] },
129
+ times: 2
130
+ )
131
+ expect_http_call_to(
132
+ path: '/v1/games/skyrimspecialedition/mods/2015/files.json',
133
+ json: { files: [json_mod_file2487] }
134
+ )
135
+ mod_files20141 = nexus_mods.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014)
136
+ mod_files20151 = nexus_mods.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2015)
137
+ mod_files20142 = nexus_mods.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014, clear_cache: true)
138
+ mod_files20152 = nexus_mods.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2015)
139
+ expect(mod_files20141).to eq(mod_files20142)
140
+ expect(mod_files20151).to eq(mod_files20152)
141
+ end
142
+
88
143
  context 'with file persistence' do
89
144
 
90
145
  it 'persists API cache in a file' do
@@ -116,11 +171,54 @@ describe NexusMods do
116
171
  # Generate the cache first
117
172
  games = nexus_mods(api_cache_file:).games
118
173
  # Force a new instance of NexusMods API to run
119
- @nexus_mods = nil
174
+ reset_nexus_mods
120
175
  expect(nexus_mods(api_cache_file:).games).to eq games
121
176
  end
122
177
  end
123
178
 
179
+ it 'uses API cache from a file, taking expiry time into account' do
180
+ with_api_cache_file do |api_cache_file|
181
+ expect_validate_user(times: 2)
182
+ expect_http_call_to(
183
+ path: '/v1/games.json',
184
+ json: [
185
+ json_game100,
186
+ json_game101
187
+ ],
188
+ times: 2
189
+ )
190
+ # Generate the cache first
191
+ games = nexus_mods(api_cache_file:, api_cache_expiry: { games: 1 }).games
192
+ # Force a new instance of NexusMods API to run
193
+ reset_nexus_mods
194
+ sleep 2
195
+ # As the expiry time is 1 second, then the cache should still be invalidated
196
+ expect(nexus_mods(api_cache_file:, api_cache_expiry: { games: 1 }).games).to eq games
197
+ end
198
+ end
199
+
200
+ it 'uses API cache from a file, taking expiry time of the new process into account' do
201
+ with_api_cache_file do |api_cache_file|
202
+ expect_validate_user(times: 2)
203
+ expect_http_call_to(
204
+ path: '/v1/games.json',
205
+ json: [
206
+ json_game100,
207
+ json_game101
208
+ ],
209
+ times: 2
210
+ )
211
+ # Generate the cache first
212
+ games = nexus_mods(api_cache_file:, api_cache_expiry: { games: 10 }).games
213
+ # Force a new instance of NexusMods API to run
214
+ reset_nexus_mods
215
+ sleep 2
216
+ # Even if the expiry time was 10 seconds while fetching the resource,
217
+ # if we decide it has to be 1 second now then it has to be invalidated.
218
+ expect(nexus_mods(api_cache_file:, api_cache_expiry: { games: 1 }).games).to eq games
219
+ end
220
+ end
221
+
124
222
  it 'completes the API cache from a file' do
125
223
  with_api_cache_file do |api_cache_file|
126
224
  expect_validate_user(times: 3)
@@ -134,7 +232,7 @@ describe NexusMods do
134
232
  )
135
233
  games = nexus_mods(api_cache_file:).games
136
234
  # Force a new instance of NexusMods API to run
137
- @nexus_mods = nil
235
+ reset_nexus_mods
138
236
  # Complete the cache with a mod
139
237
  expect_http_call_to(
140
238
  path: '/v1/games/skyrimspecialedition/mods/2014.json',
@@ -142,7 +240,7 @@ describe NexusMods do
142
240
  )
143
241
  mod = nexus_mods(api_cache_file:).mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014)
144
242
  # Force a new instance of NexusMods API to run
145
- @nexus_mods = nil
243
+ reset_nexus_mods
146
244
  # Check that both API calls were cached correctly
147
245
  nexus_mods_instance = nexus_mods(api_cache_file:)
148
246
  expect(nexus_mods_instance.games).to eq games
@@ -150,6 +248,41 @@ describe NexusMods do
150
248
  end
151
249
  end
152
250
 
251
+ it 'clears the cache of a wanted resource in the API cache file as well' do
252
+ with_api_cache_file do |api_cache_file|
253
+ expect_validate_user(times: 3)
254
+ # Generate the cache first for 2 mod files queries
255
+ expect_http_call_to(
256
+ path: '/v1/games/skyrimspecialedition/mods/2014/files.json',
257
+ json: { files: [json_mod_file2472] },
258
+ times: 2
259
+ )
260
+ expect_http_call_to(
261
+ path: '/v1/games/skyrimspecialedition/mods/2015/files.json',
262
+ json: { files: [json_mod_file2487] }
263
+ )
264
+ nexus_mods_instance1 = nexus_mods(api_cache_file:)
265
+ mod_files20141 = nexus_mods_instance1.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014)
266
+ mod_files20151 = nexus_mods_instance1.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2015)
267
+ # Force a new instance of NexusMods API to run
268
+ reset_nexus_mods
269
+ # Clear the cache of the first API query
270
+ nexus_mods_instance2 = nexus_mods(api_cache_file:)
271
+ mod_files20142 = nexus_mods_instance2.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014, clear_cache: true)
272
+ mod_files20152 = nexus_mods_instance2.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2015)
273
+ # Force a new instance of NexusMods API to run
274
+ reset_nexus_mods
275
+ # Get again the data, it should have been in the cache already
276
+ nexus_mods_instance3 = nexus_mods(api_cache_file:)
277
+ mod_files20143 = nexus_mods_instance3.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014)
278
+ mod_files20153 = nexus_mods_instance3.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2015)
279
+ expect(mod_files20141).to eq(mod_files20142)
280
+ expect(mod_files20141).to eq(mod_files20143)
281
+ expect(mod_files20151).to eq(mod_files20152)
282
+ expect(mod_files20151).to eq(mod_files20153)
283
+ end
284
+ end
285
+
153
286
  end
154
287
 
155
288
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nexus_mods
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan