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 +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/nexus_mods/api_client.rb +78 -57
- data/lib/nexus_mods/cacheable_api.rb +8 -2
- data/lib/nexus_mods/cacheable_with_expiry.rb +14 -12
- data/lib/nexus_mods/core_extensions/cacheable/cache_adapters/persistent_json_adapter.rb +38 -2
- data/lib/nexus_mods/version.rb +1 -1
- data/lib/nexus_mods.rb +18 -6
- data/spec/nexus_mods_test/helpers.rb +42 -2
- data/spec/nexus_mods_test/scenarios/nexus_mods_caching_spec.rb +202 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 758229de2b795e860d8b150e4bbc8a2d516f6e49c2773ac242c0b67472f6c157
|
4
|
+
data.tar.gz: 96e38216e7f54b9198c9362bb01b7ea0fdde2205e357a419123b3337c2791ee4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
:
|
152
|
+
:cached_api,
|
89
153
|
expiry_from_key: proc do |key|
|
90
154
|
# Example of keys:
|
91
|
-
# NexusMods::ApiClient/
|
92
|
-
# NexusMods::ApiClient/
|
93
|
-
# NexusMods::ApiClient/
|
94
|
-
# NexusMods::ApiClient/
|
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
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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):
|
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):
|
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
|
data/lib/nexus_mods/version.rb
CHANGED
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
|
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
|