nexus_mods 0.3.0 → 0.5.0

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: 7858789972e8674c6ba24c53d36e4af87b09b8636e84faced240ff0edd1eb3fa
4
- data.tar.gz: 29df7fc4849e885e6a9aa705d1ae4868215e1181bdff8814d806787ff948790d
3
+ metadata.gz: 758229de2b795e860d8b150e4bbc8a2d516f6e49c2773ac242c0b67472f6c157
4
+ data.tar.gz: 96e38216e7f54b9198c9362bb01b7ea0fdde2205e357a419123b3337c2791ee4
5
5
  SHA512:
6
- metadata.gz: 522c72b5d4cabff4d57609f9c593c5e75345c675343397e7e75c0bc10c7b29c8df72c26d7fa51bc73265bb8f0c025d3b929ef5a1810711928f416b851bedfef9
7
- data.tar.gz: 17b804dfc30795f57c5e862a4990b39aec762f0b3058513956204443e2ebfe59df5285ceda0634a6644cdcd8e45619a8248964bf3028b2aa96852cbc4dd69f9f
6
+ metadata.gz: 8c2fa3106f59e59472bcb39d107d28bba9ce394e9c7d2783d5b60e0074ea7070bfd175aaa1dd543397b057aaabd7bd0778f299609acbf30a706870f4460c95ab
7
+ data.tar.gz: f2f6994f254288a5c8b7ba806a3f0b9b18a044aacdf9b7edd381b629a6e97c9d49f881ba566437beb4739cf32a2a0b11575fd71f574210e62763fb5fcc86b8cb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
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
+
7
+ # [v0.4.0](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.3.0...v0.4.0) (2023-04-10 10:15:41)
8
+
9
+ ### Features
10
+
11
+ * [[Feature] Add persistent API caching in files](https://github.com/Muriel-Salvan/nexus_mods/commit/f124ecf58b0f478ecdca5f6ad2309779d1ab67ac)
12
+
1
13
  # [v0.3.0](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.2.0...v0.3.0) (2023-04-10 09:13:43)
2
14
 
3
15
  ### Features
@@ -12,6 +12,13 @@ class NexusMods
12
12
 
13
13
  include CacheableApi
14
14
 
15
+ # Default expiry times, in seconds
16
+ DEFAULT_API_CACHE_EXPIRY = {
17
+ games: 24 * 60 * 60,
18
+ mod: 24 * 60 * 60,
19
+ mod_files: 24 * 60 * 60
20
+ }
21
+
15
22
  # Constructor
16
23
  #
17
24
  # Parameters::
@@ -31,8 +38,9 @@ class NexusMods
31
38
  logger: Logger.new($stdout)
32
39
  )
33
40
  @api_key = api_key
34
- ApiClient.api_cache_expiry = ApiClient.api_cache_expiry.merge(api_cache_expiry)
41
+ @api_cache_expiry = DEFAULT_API_CACHE_EXPIRY.merge(api_cache_expiry)
35
42
  @api_cache_file = api_cache_file
43
+ ApiClient.api_client = self
36
44
  @logger = logger
37
45
  # Initialize our HTTP client
38
46
  @http_cache = http_cache_file.nil? ? nil : FileCache.new(http_cache_file)
@@ -55,9 +63,65 @@ class NexusMods
55
63
  # Parameters::
56
64
  # * *path* (String): API path to contact (from v1/ and without .json)
57
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]
58
67
  # Result::
59
68
  # * Object: The JSON response
60
- 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)
61
125
  res = http(path, verb:)
62
126
  json = JSON.parse(res.body)
63
127
  uri = api_uri(path)
@@ -85,26 +149,26 @@ class NexusMods
85
149
  json
86
150
  end
87
151
  cacheable_api(
88
- :api,
152
+ :cached_api,
89
153
  expiry_from_key: proc do |key|
90
154
  # Example of keys:
91
- # NexusMods::ApiClient/api/games
92
- # NexusMods::ApiClient/api/games/skyrimspecialedition/mods/2014
93
- # NexusMods::ApiClient/api/games/skyrimspecialedition/mods/2014/files
94
- # NexusMods::ApiClient/api/users/validate
95
- 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]
96
160
  case key_components[0]
97
161
  when 'games'
98
162
  if key_components[1].nil?
99
- ApiClient.api_cache_expiry[:games]
163
+ ApiClient.api_client.api_cache_expiry[:games]
100
164
  else
101
165
  case key_components[2]
102
166
  when 'mods'
103
167
  case key_components[4]
104
168
  when nil
105
- ApiClient.api_cache_expiry[:mod]
169
+ ApiClient.api_client.api_cache_expiry[:mod]
106
170
  when 'files'
107
- ApiClient.api_cache_expiry[:mod_files]
171
+ ApiClient.api_client.api_cache_expiry[:mod_files]
108
172
  else
109
173
  raise "Unknown API path: #{key}"
110
174
  end
@@ -118,55 +182,12 @@ class NexusMods
118
182
  else
119
183
  raise "Unknown API path: #{key}"
120
184
  end
185
+ end,
186
+ on_cache_update: proc do
187
+ ApiClient.api_client.save_api_cache
121
188
  end
122
189
  )
123
190
 
124
- # Send an HTTP request to the API and get back the HTTP response
125
- #
126
- # Parameters::
127
- # * *path* (String): API path to contact (from v1/ and without .json)
128
- # * *verb* (Symbol): Verb to be used (:get, :post...) [default: :get]
129
- # Result::
130
- # * Faraday::Response: The HTTP response
131
- def http(path, verb: :get)
132
- @http_client.send(verb) do |req|
133
- req.url api_uri(path)
134
- req.headers['apikey'] = @api_key
135
- req.headers['User-Agent'] = "nexus_mods (#{RUBY_PLATFORM}) Ruby/#{RUBY_VERSION}"
136
- end
137
- end
138
-
139
- # Load the API cache if a file was given to this client
140
- def load_api_cache
141
- Cacheable.cache_adapter.load(@api_cache_file) if @api_cache_file && File.exist?(@api_cache_file)
142
- end
143
-
144
- # Save the API cache if a file was given to this client
145
- def save_api_cache
146
- return unless @api_cache_file
147
-
148
- FileUtils.mkdir_p(File.dirname(@api_cache_file))
149
- Cacheable.cache_adapter.dump(@api_cache_file)
150
- end
151
-
152
- private
153
-
154
- class << self
155
-
156
- # Hash<Symbol,Integer>: Expiry time, per expiry key
157
- attr_accessor :api_cache_expiry
158
-
159
- end
160
-
161
- # Default expiry times, in seconds
162
- DEFAULT_API_CACHE_EXPIRY = {
163
- games: 24 * 60 * 60,
164
- mod: 24 * 60 * 60,
165
- mod_files: 24 * 60 * 60
166
- }
167
-
168
- @api_cache_expiry = DEFAULT_API_CACHE_EXPIRY
169
-
170
191
  # Get the real URI to query for a given API path
171
192
  #
172
193
  # Parameters::
@@ -28,7 +28,8 @@ class NexusMods
28
28
  # * *key* (String): The key for which we want the expiry time in seconds
29
29
  # * Result::
30
30
  # * Integer: Corresponding expiry time
31
- def cacheable_api(*original_method_names, expiry_from_key:)
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
33
  cacheable_with_expiry(
33
34
  *original_method_names,
34
35
  key_format: lambda do |target, method_name, method_args, method_kwargs|
@@ -41,7 +42,12 @@ class NexusMods
41
42
  method_kwargs.map { |key, value| "#{key}:#{value}" }
42
43
  ).join('/')
43
44
  end,
44
- expiry_from_key:
45
+ expiry_from_key:,
46
+ cache_options: {
47
+ on_cache_update: proc do |_adapter, _key, _value, _options, _context|
48
+ on_cache_update.call
49
+ end
50
+ }
45
51
  )
46
52
  end
47
53
 
@@ -34,19 +34,21 @@ class NexusMods
34
34
  *original_method_names,
35
35
  **opts.merge(
36
36
  {
37
- cache_options: {
38
- expiry_from_key: opts[:expiry_from_key],
39
- invalidate_if: proc do |key, options, context|
40
- next true unless context['invalidate_time']
37
+ cache_options: (opts[:cache_options] || {}).merge(
38
+ {
39
+ expiry_from_key: opts[:expiry_from_key],
40
+ invalidate_if: proc do |key, options, context|
41
+ next true unless context['invalidate_time']
41
42
 
42
- # Find if we know already the expiry for this key
43
- expiry_cache[key] = options[:expiry_from_key].call(key) unless expiry_cache.key?(key)
44
- expiry_cache[key].nil? || (Time.now.utc - Time.parse(context['invalidate_time']).utc > expiry_cache[key])
45
- end,
46
- update_context_after_fetch: proc do |_key, _value, _options, context|
47
- context['invalidate_time'] = Time.now.utc.strftime('%FT%TUTC')
48
- end
49
- }
43
+ # Find if we know already the expiry for this key
44
+ expiry_cache[key] = options[:expiry_from_key].call(key) unless expiry_cache.key?(key)
45
+ expiry_cache[key].nil? || (Time.now.utc - Time.parse(context['invalidate_time']).utc > expiry_cache[key])
46
+ end,
47
+ update_context_after_fetch: proc do |_key, _value, _options, context|
48
+ context['invalidate_time'] = Time.now.utc.strftime('%FT%TUTC')
49
+ end
50
+ }
51
+ )
50
52
  }
51
53
  )
52
54
  )
@@ -18,19 +18,26 @@ module Cacheable
18
18
  # Parameters::
19
19
  # * *key* (String): Key to be fetched
20
20
  # * *options* (Hash): Cache options. The following options are interpreted by this fetch:
21
- # * *invalidate_if* (Proc): Code called to know if the cache should be invalidated for a given key:
21
+ # * *invalidate_if* (Proc or nil): Optional code called to know if the cache should be invalidated for a given key:
22
22
  # * Parameters::
23
23
  # * *key* (String): The key for which we check the cache invalidation
24
24
  # * *options* (Hash): Cache options linked to this key
25
25
  # * *key_context* (Hash): Context linked to this key, that can be set using the update_context_after_fetch callback.
26
26
  # * Result::
27
27
  # * Boolean: Should we invalidate the cached value of this key?
28
- # * *update_context_after_fetch* (Proc): Code called when the value has been fetched for real (without cache), used to update the context
28
+ # * *update_context_after_fetch* (Proc or nil): Optional code called when the value has been fetched for real (without cache), used to update the context
29
29
  # * Parameters::
30
30
  # * *key* (String): The key for which we just fetched the value
31
31
  # * *value* (Object): The value that has just been fetched
32
32
  # * *options* (Hash): Cache options linked to this key
33
33
  # * *key_context* (Hash): Context linked to this key, that is supposed to be updated in place by this callback
34
+ # * *on_cache_update* (Proc or nil): Optional code called once the cache has been updated
35
+ # * Parameters::
36
+ # * *adapter* (Object): Adapter that has the cache being updated
37
+ # * *key* (String): The key for which we just fetched the value
38
+ # * *value* (Object): The value that has just been fetched
39
+ # * *options* (Hash): Cache options linked to this key
40
+ # * *key_context* (Hash): Context linked to this key
34
41
  # * CodeBlock: Code called to fetch the value if not in the cache
35
42
  # Result::
36
43
  # * Object: The value for this key
@@ -43,6 +50,8 @@ module Cacheable
43
50
  value = yield
44
51
  options[:update_context_after_fetch]&.call(key, value, options, key_context)
45
52
  write(key, value)
53
+ options[:on_cache_update]&.call(self, key, value, options, key_context)
54
+ value
46
55
  end
47
56
 
48
57
  # Clear the cache
@@ -51,6 +60,33 @@ module Cacheable
51
60
  super
52
61
  end
53
62
 
63
+ # Save the cache and context into a JSON file
64
+ #
65
+ # Parameters::
66
+ # * *file* (String): The file to save to
67
+ def save(file)
68
+ # Remove from the context the keys that are not in the cache
69
+ File.write(
70
+ file,
71
+ JSON.dump(
72
+ {
73
+ 'cache' => cache,
74
+ 'context' => context.select { |key, _value| @cache.key?(key) }
75
+ }
76
+ )
77
+ )
78
+ end
79
+
80
+ # Load the cache and context from a JSON file
81
+ #
82
+ # Parameters::
83
+ # * *file* (String): The file to load from
84
+ def load(file)
85
+ loaded_content = JSON.parse(File.read(file))
86
+ @cache = loaded_content['cache']
87
+ @context = loaded_content['context']
88
+ end
89
+
54
90
  private
55
91
 
56
92
  attr_reader :context
@@ -1,5 +1,5 @@
1
1
  class NexusMods
2
2
 
3
- VERSION = '0.3.0'
3
+ VERSION = '0.5.0'
4
4
 
5
5
  end
data/lib/nexus_mods.rb CHANGED
@@ -44,6 +44,7 @@ class NexusMods
44
44
  # * *games*: Expiry associated to queries on games [default: 1 day]
45
45
  # * *mod*: Expiry associated to queries on mod [default: 1 day]
46
46
  # * *mod_files*: Expiry associated to queries on mod files [default: 1 day]
47
+ # * *api_cache_file* (String): File used to store the NexusMods API cache, or nil for no cache [default: "#{Dir.tmpdir}/nexus_mods_api_cache.json"]
47
48
  # * *logger* (Logger): The logger to be used for log messages [default: Logger.new(STDOUT)]
48
49
  def initialize(
49
50
  api_key: nil,
@@ -52,6 +53,7 @@ class NexusMods
52
53
  file_id: 1,
53
54
  http_cache_file: "#{Dir.tmpdir}/nexus_mods_http_cache.json",
54
55
  api_cache_expiry: {},
56
+ api_cache_file: "#{Dir.tmpdir}/nexus_mods_api_cache.json",
55
57
  logger: Logger.new($stdout)
56
58
  )
57
59
  @game_domain_name = game_domain_name
@@ -63,6 +65,7 @@ class NexusMods
63
65
  api_key:,
64
66
  http_cache_file:,
65
67
  api_cache_expiry:,
68
+ api_cache_file:,
66
69
  logger:
67
70
  )
68
71
 
@@ -95,10 +98,12 @@ class NexusMods
95
98
 
96
99
  # Get the list of games
97
100
  #
101
+ # Parameters::
102
+ # * *clear_cache* (Boolean): Should we clear the API cache for this resource? [default: false]
98
103
  # Result::
99
104
  # * Array<Game>: List of games
100
- def games
101
- @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|
102
107
  # First create categories tree
103
108
  # Hash<Integer, [Category, Integer]>: Category and its parent category id, per category id
104
109
  categories = game_json['categories'].to_h do |category_json|
@@ -142,10 +147,11 @@ class NexusMods
142
147
  # Parameters::
143
148
  # * *game_domain_name* (String): Game domain name to query by default [default: @game_domain_name]
144
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]
145
151
  # Result::
146
152
  # * Mod: Mod information
147
- def mod(game_domain_name: @game_domain_name, mod_id: @mod_id)
148
- 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:)
149
155
  Api::Mod.new(
150
156
  uid: mod_json['uid'],
151
157
  mod_id: mod_json['mod_id'],
@@ -190,10 +196,11 @@ class NexusMods
190
196
  # Parameters::
191
197
  # * *game_domain_name* (String): Game domain name to query by default [default: @game_domain_name]
192
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]
193
200
  # Result::
194
201
  # * Array<ModFile>: List of mod's files
195
- def mod_files(game_domain_name: @game_domain_name, mod_id: @mod_id)
196
- @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|
197
204
  category_id = FILE_CATEGORIES[file_json['category_id']]
198
205
  raise "Unknown file category: #{file_json['category_id']}" if category_id.nil?
199
206
 
@@ -218,4 +225,9 @@ class NexusMods
218
225
  end
219
226
  end
220
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
+
221
233
  end
@@ -55,14 +55,28 @@ module NexusModsTest
55
55
  def nexus_mods(**args)
56
56
  if @nexus_mods.nil?
57
57
  args[:api_key] = MOCKED_API_KEY unless args.key?(:api_key)
58
+ # By default running tests should not persistent cache files
59
+ args[:http_cache_file] = nil unless args.key?(:http_cache_file)
60
+ args[:api_cache_file] = nil unless args.key?(:api_cache_file)
58
61
  # Redirect any log into a string so that they don't pollute the tests output and they could be asserted.
59
- nexus_mods_logger = StringIO.new
60
- args[:logger] = Logger.new(nexus_mods_logger)
62
+ @nexus_mods_logger = StringIO.new
63
+ args[:logger] = Logger.new(@nexus_mods_logger)
61
64
  @nexus_mods = NexusMods.new(**args)
62
65
  end
63
66
  @nexus_mods
64
67
  end
65
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
+
66
80
  # Expect an HTTP API call to be made, and mock the corresponding HTTP response.
67
81
  # Handle the API key and user agent.
68
82
  #
@@ -134,6 +148,27 @@ module NexusModsTest
134
148
  )
135
149
  end
136
150
 
151
+ # Setup an API cache file to be used (does not create it)
152
+ #
153
+ # Parameters::
154
+ # * CodeBlock: The code called when the API cache file is reserved
155
+ # * Parameters::
156
+ # * *api_cache_file* (String): File name to be used for the API cache file
157
+ def with_api_cache_file
158
+ api_cache_file = "#{Dir.tmpdir}/nexus_mods_test/api_cache.json"
159
+ FileUtils.mkdir_p(File.dirname(api_cache_file))
160
+ FileUtils.rm_f(api_cache_file)
161
+ yield api_cache_file
162
+ end
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
+
137
172
  end
138
173
 
139
174
  end
@@ -159,6 +194,11 @@ RSpec.configure do |config|
159
194
  expect(stub).to have_been_made.times(times)
160
195
  end
161
196
  end
197
+ config.around do |example|
198
+ example.call
199
+ ensure
200
+ reset_nexus_mods
201
+ end
162
202
  end
163
203
 
164
204
  # Set a bigger output length for expectation errors, as most error messages include API keys and headers which can be lengthy
@@ -1,3 +1,5 @@
1
+ require 'fileutils'
2
+
1
3
  describe NexusMods do
2
4
 
3
5
  context 'when testing caching' do
@@ -21,6 +23,20 @@ describe NexusMods do
21
23
  expect(nexus_mods.games).to eq(games)
22
24
  end
23
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
+
24
40
  it 'caches mod queries' do
25
41
  expect_validate_user
26
42
  expect_http_call_to(
@@ -31,6 +47,17 @@ describe NexusMods do
31
47
  expect(nexus_mods.mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq(mod)
32
48
  end
33
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
+
34
61
  it 'caches mod files queries' do
35
62
  expect_validate_user
36
63
  expect_http_call_to(
@@ -41,6 +68,17 @@ describe NexusMods do
41
68
  expect(nexus_mods.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq(mod_files)
42
69
  end
43
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
+
44
82
  it 'expires games queries cache' do
45
83
  expect_validate_user
46
84
  expect_http_call_to(
@@ -83,6 +121,170 @@ describe NexusMods do
83
121
  expect(nexus_mods_instance.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq(mod_files)
84
122
  end
85
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
+
143
+ context 'with file persistence' do
144
+
145
+ it 'persists API cache in a file' do
146
+ with_api_cache_file do |api_cache_file|
147
+ expect_validate_user
148
+ expect_http_call_to(
149
+ path: '/v1/games.json',
150
+ json: [
151
+ json_game100,
152
+ json_game101
153
+ ]
154
+ )
155
+ nexus_mods(api_cache_file:).games
156
+ expect(File.exist?(api_cache_file)).to be true
157
+ expect(File.size(api_cache_file)).to be > 0
158
+ end
159
+ end
160
+
161
+ it 'uses API cache from a file' do
162
+ with_api_cache_file do |api_cache_file|
163
+ expect_validate_user(times: 2)
164
+ expect_http_call_to(
165
+ path: '/v1/games.json',
166
+ json: [
167
+ json_game100,
168
+ json_game101
169
+ ]
170
+ )
171
+ # Generate the cache first
172
+ games = nexus_mods(api_cache_file:).games
173
+ # Force a new instance of NexusMods API to run
174
+ reset_nexus_mods
175
+ expect(nexus_mods(api_cache_file:).games).to eq games
176
+ end
177
+ end
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
+
222
+ it 'completes the API cache from a file' do
223
+ with_api_cache_file do |api_cache_file|
224
+ expect_validate_user(times: 3)
225
+ # Generate the cache first for games only
226
+ expect_http_call_to(
227
+ path: '/v1/games.json',
228
+ json: [
229
+ json_game100,
230
+ json_game101
231
+ ]
232
+ )
233
+ games = nexus_mods(api_cache_file:).games
234
+ # Force a new instance of NexusMods API to run
235
+ reset_nexus_mods
236
+ # Complete the cache with a mod
237
+ expect_http_call_to(
238
+ path: '/v1/games/skyrimspecialedition/mods/2014.json',
239
+ json: json_complete_mod
240
+ )
241
+ mod = nexus_mods(api_cache_file:).mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014)
242
+ # Force a new instance of NexusMods API to run
243
+ reset_nexus_mods
244
+ # Check that both API calls were cached correctly
245
+ nexus_mods_instance = nexus_mods(api_cache_file:)
246
+ expect(nexus_mods_instance.games).to eq games
247
+ expect(nexus_mods_instance.mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq mod
248
+ end
249
+ end
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
+
286
+ end
287
+
86
288
  end
87
289
 
88
290
  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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan