nexus_mods 0.4.0 → 0.5.1

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: 79e4d81970dd1549db43105a2a155b6b275692df4f06827e2234ab02fce6276b
4
- data.tar.gz: 4d96df8a39784c8a2eb6fb9bd72a247f3cafa2765aa03059dfc661afd9db7a38
3
+ metadata.gz: 30a5d88705ee7a8d0198ee6bd944bcc8735468e162e8bf17877ecffa3a356d27
4
+ data.tar.gz: c622e319de62f51ee8dd8a8615187105dc8d246efe926933037315798e003ac0
5
5
  SHA512:
6
- metadata.gz: 9c13d188694e09dc06998846791322a4887bbb5163ff634d5e6f813abca187a6a2dc8aeb102e040b43812d6276feb6c1ee59665c5e5e408c6acbff4dca0539b3
7
- data.tar.gz: 3e37eb153f00bb6335efb8e0b377065f4b0613cbef46066a1a5d7095b1e1767b1786d59f28681298e75be49e1cfeb739ea6ce2f7b13fefc6b28362b27d7536b5
6
+ metadata.gz: c43800296d117d5e07862753eb2325d86549550237ab66c7a8b50d22b16676821cedbb69deea59dbca947266f74c9ba513683cb5846a807fdb338c4468154c81
7
+ data.tar.gz: d172bd4112bfec791136a5f6b4e608871c92da6f6b3d025337dc767f64b712743173666458e767c57b308d328dbb91409eb7fc4261e92d42c048d9ed1b645eb3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [v0.5.1](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.5.0...v0.5.1) (2023-04-10 17:19:39)
2
+
3
+ ### Patches
4
+
5
+ * [Use more efficient API cache keys](https://github.com/Muriel-Salvan/nexus_mods/commit/3f09a08b815c71a0b0ec6694359c02fd8503bc90)
6
+
7
+ # [v0.5.0](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.4.0...v0.5.0) (2023-04-10 17:05:11)
8
+
9
+ ### Features
10
+
11
+ * [[Feature] Add clear_cache parameters to invalidate the API cache at will](https://github.com/Muriel-Salvan/nexus_mods/commit/44958e35eae20ef818e3d4735cc3bd3008cfc5df)
12
+
1
13
  # [v0.4.0](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.3.0...v0.4.0) (2023-04-10 10:15:41)
2
14
 
3
15
  ### 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,17 @@ class NexusMods
93
149
  json
94
150
  end
95
151
  cacheable_api(
96
- :api,
152
+ :cached_api,
153
+ key_format: proc do |_target, _method_name, method_args, method_kwargs|
154
+ "#{method_kwargs[:verb]}/#{method_args.first}"
155
+ end,
97
156
  expiry_from_key: proc do |key|
98
157
  # 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..]
158
+ # get/games
159
+ # get/games/skyrimspecialedition/mods/2014
160
+ # get/games/skyrimspecialedition/mods/2014/files
161
+ # get/users/validate
162
+ key_components = key.split('/')[1..]
104
163
  case key_components[0]
105
164
  when 'games'
106
165
  if key_components[1].nil?
@@ -132,48 +191,6 @@ class NexusMods
132
191
  end
133
192
  )
134
193
 
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
194
  # Get the real URI to query for a given API path
178
195
  #
179
196
  # Parameters::
@@ -29,10 +29,19 @@ class NexusMods
29
29
  # * Result::
30
30
  # * Integer: Corresponding expiry time
31
31
  # * *on_cache_update* (Proc): Proc called when the cache has been updated
32
- def cacheable_api(*original_method_names, expiry_from_key:, on_cache_update:)
32
+ # * *key_format* (Proc or nil): Optional proc giving the key format from the target of cacheable [default: nil].
33
+ # If nil then a default proc concatenating the target's class, method name and all arguments will be used.
34
+ # * Parameters::
35
+ # * *target* (Object): Object on which the method is cached
36
+ # * *method_name* (Symbol): Method being cached
37
+ # * *method_args* (Array<Object>): Method's arguments
38
+ # * *method_kwargs* (Hash<Symbol,Object>): Method's kwargs
39
+ # * Result::
40
+ # * String: The corresponding key to be used for caching
41
+ def cacheable_api(*original_method_names, expiry_from_key:, on_cache_update:, key_format: nil)
33
42
  cacheable_with_expiry(
34
43
  *original_method_names,
35
- key_format: lambda do |target, method_name, method_args, method_kwargs|
44
+ key_format: key_format || proc do |target, method_name, method_args, method_kwargs|
36
45
  (
37
46
  [
38
47
  target.class,
@@ -1,5 +1,5 @@
1
1
  class NexusMods
2
2
 
3
- VERSION = '0.4.0'
3
+ VERSION = '0.5.1'
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
 
@@ -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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan