nexus_mods 0.3.0 → 0.4.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: 79e4d81970dd1549db43105a2a155b6b275692df4f06827e2234ab02fce6276b
4
+ data.tar.gz: 4d96df8a39784c8a2eb6fb9bd72a247f3cafa2765aa03059dfc661afd9db7a38
5
5
  SHA512:
6
- metadata.gz: 522c72b5d4cabff4d57609f9c593c5e75345c675343397e7e75c0bc10c7b29c8df72c26d7fa51bc73265bb8f0c025d3b929ef5a1810711928f416b851bedfef9
7
- data.tar.gz: 17b804dfc30795f57c5e862a4990b39aec762f0b3058513956204443e2ebfe59df5285ceda0634a6644cdcd8e45619a8248964bf3028b2aa96852cbc4dd69f9f
6
+ metadata.gz: 9c13d188694e09dc06998846791322a4887bbb5163ff634d5e6f813abca187a6a2dc8aeb102e040b43812d6276feb6c1ee59665c5e5e408c6acbff4dca0539b3
7
+ data.tar.gz: 3e37eb153f00bb6335efb8e0b377065f4b0613cbef46066a1a5d7095b1e1767b1786d59f28681298e75be49e1cfeb739ea6ce2f7b13fefc6b28362b27d7536b5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # [v0.4.0](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.3.0...v0.4.0) (2023-04-10 10:15:41)
2
+
3
+ ### Features
4
+
5
+ * [[Feature] Add persistent API caching in files](https://github.com/Muriel-Salvan/nexus_mods/commit/f124ecf58b0f478ecdca5f6ad2309779d1ab67ac)
6
+
1
7
  # [v0.3.0](https://github.com/Muriel-Salvan/nexus_mods/compare/v0.2.0...v0.3.0) (2023-04-10 09:13:43)
2
8
 
3
9
  ### 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)
@@ -96,15 +104,15 @@ class NexusMods
96
104
  case key_components[0]
97
105
  when 'games'
98
106
  if key_components[1].nil?
99
- ApiClient.api_cache_expiry[:games]
107
+ ApiClient.api_client.api_cache_expiry[:games]
100
108
  else
101
109
  case key_components[2]
102
110
  when 'mods'
103
111
  case key_components[4]
104
112
  when nil
105
- ApiClient.api_cache_expiry[:mod]
113
+ ApiClient.api_client.api_cache_expiry[:mod]
106
114
  when 'files'
107
- ApiClient.api_cache_expiry[:mod_files]
115
+ ApiClient.api_client.api_cache_expiry[:mod_files]
108
116
  else
109
117
  raise "Unknown API path: #{key}"
110
118
  end
@@ -118,6 +126,9 @@ class NexusMods
118
126
  else
119
127
  raise "Unknown API path: #{key}"
120
128
  end
129
+ end,
130
+ on_cache_update: proc do
131
+ ApiClient.api_client.save_api_cache
121
132
  end
122
133
  )
123
134
 
@@ -146,26 +157,22 @@ class NexusMods
146
157
  return unless @api_cache_file
147
158
 
148
159
  FileUtils.mkdir_p(File.dirname(@api_cache_file))
149
- Cacheable.cache_adapter.dump(@api_cache_file)
160
+ Cacheable.cache_adapter.save(@api_cache_file)
150
161
  end
151
162
 
163
+ # Some attributes exposed for the cacheable feature to work
164
+ attr_reader :api_cache_expiry
165
+
152
166
  private
153
167
 
154
168
  class << self
155
169
 
156
- # Hash<Symbol,Integer>: Expiry time, per expiry key
157
- attr_accessor :api_cache_expiry
170
+ # ApiClient: The API client to be used by the cacheable adapter (singleton pattern)
171
+ attr_accessor :api_client
158
172
 
159
173
  end
160
174
 
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
175
+ @api_client = nil
169
176
 
170
177
  # Get the real URI to query for a given API path
171
178
  #
@@ -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.4.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
 
@@ -55,6 +55,9 @@ 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
62
  nexus_mods_logger = StringIO.new
60
63
  args[:logger] = Logger.new(nexus_mods_logger)
@@ -134,6 +137,19 @@ module NexusModsTest
134
137
  )
135
138
  end
136
139
 
140
+ # Setup an API cache file to be used (does not create it)
141
+ #
142
+ # Parameters::
143
+ # * CodeBlock: The code called when the API cache file is reserved
144
+ # * Parameters::
145
+ # * *api_cache_file* (String): File name to be used for the API cache file
146
+ def with_api_cache_file
147
+ api_cache_file = "#{Dir.tmpdir}/nexus_mods_test/api_cache.json"
148
+ FileUtils.mkdir_p(File.dirname(api_cache_file))
149
+ FileUtils.rm_f(api_cache_file)
150
+ yield api_cache_file
151
+ end
152
+
137
153
  end
138
154
 
139
155
  end
@@ -1,3 +1,5 @@
1
+ require 'fileutils'
2
+
1
3
  describe NexusMods do
2
4
 
3
5
  context 'when testing caching' do
@@ -83,6 +85,73 @@ describe NexusMods do
83
85
  expect(nexus_mods_instance.mod_files(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq(mod_files)
84
86
  end
85
87
 
88
+ context 'with file persistence' do
89
+
90
+ it 'persists API cache in a file' do
91
+ with_api_cache_file do |api_cache_file|
92
+ expect_validate_user
93
+ expect_http_call_to(
94
+ path: '/v1/games.json',
95
+ json: [
96
+ json_game100,
97
+ json_game101
98
+ ]
99
+ )
100
+ nexus_mods(api_cache_file:).games
101
+ expect(File.exist?(api_cache_file)).to be true
102
+ expect(File.size(api_cache_file)).to be > 0
103
+ end
104
+ end
105
+
106
+ it 'uses API cache from a file' do
107
+ with_api_cache_file do |api_cache_file|
108
+ expect_validate_user(times: 2)
109
+ expect_http_call_to(
110
+ path: '/v1/games.json',
111
+ json: [
112
+ json_game100,
113
+ json_game101
114
+ ]
115
+ )
116
+ # Generate the cache first
117
+ games = nexus_mods(api_cache_file:).games
118
+ # Force a new instance of NexusMods API to run
119
+ @nexus_mods = nil
120
+ expect(nexus_mods(api_cache_file:).games).to eq games
121
+ end
122
+ end
123
+
124
+ it 'completes the API cache from a file' do
125
+ with_api_cache_file do |api_cache_file|
126
+ expect_validate_user(times: 3)
127
+ # Generate the cache first for games only
128
+ expect_http_call_to(
129
+ path: '/v1/games.json',
130
+ json: [
131
+ json_game100,
132
+ json_game101
133
+ ]
134
+ )
135
+ games = nexus_mods(api_cache_file:).games
136
+ # Force a new instance of NexusMods API to run
137
+ @nexus_mods = nil
138
+ # Complete the cache with a mod
139
+ expect_http_call_to(
140
+ path: '/v1/games/skyrimspecialedition/mods/2014.json',
141
+ json: json_complete_mod
142
+ )
143
+ mod = nexus_mods(api_cache_file:).mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014)
144
+ # Force a new instance of NexusMods API to run
145
+ @nexus_mods = nil
146
+ # Check that both API calls were cached correctly
147
+ nexus_mods_instance = nexus_mods(api_cache_file:)
148
+ expect(nexus_mods_instance.games).to eq games
149
+ expect(nexus_mods_instance.mod(game_domain_name: 'skyrimspecialedition', mod_id: 2014)).to eq mod
150
+ end
151
+ end
152
+
153
+ end
154
+
86
155
  end
87
156
 
88
157
  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.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan