puppet_forge 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf36457b8298a66e2b7af77d53a0bf476b4881c2dd29de7b1e8a49a523400f53
4
- data.tar.gz: e5926f236dbe38b61bf3e1b9e96677ef352db5afabb9de836d40d1be136830f5
3
+ metadata.gz: 2eed3dead78d19c535ee9a2b372a89e17efcf7b24f01c752bb38096a2ed52e37
4
+ data.tar.gz: 2036388fc19159a57d425383aa6c89326d222a900f9fd61fbbb529e62d004f28
5
5
  SHA512:
6
- metadata.gz: d604d660f74c37ea1bdeeb048d94edf557b1037d46bc6e83963f1d8b9d1652ceb66d3aafb3b498c470ffc1083c7d2fbb5d22a410b3ae2e91e12b7381f2ee9c26
7
- data.tar.gz: fbf2a345b0c6b538a7a0424965d13aa1ab54f9e064273fe983176b60516a1076b1f3bd7b8edca3bed5272b0380de1fb6448d81dde147d31f3961c99ea49fde26
6
+ metadata.gz: ca288c45d5329d1656221858c03a1125e0e29eb1e6701b95da90baa2cd6ba254907e15fafbf4f1474b69a43014a3c9f10fbcaed620d3913d4bf221194b3a2cc5
7
+ data.tar.gz: 8f5a441289712e2cb9ef9c7d34a17cef347b2b09ff0f60fc1430cceae3780229201aecee3b0baa601a3fd343060ed1976fad247e8e54abbbfb7d5a9a37f30fc0
@@ -15,13 +15,14 @@ jobs:
15
15
  strategy:
16
16
  matrix:
17
17
  ruby:
18
+ - '3.2'
18
19
  - '3.1'
19
20
  - '3.0'
20
21
  - '2.7'
21
22
  - '2.6'
22
23
 
23
24
  steps:
24
- - uses: actions/checkout@v1
25
+ - uses: actions/checkout@v3
25
26
  - name: Set up Ruby ${{ matrix.ruby }}
26
27
  uses: ruby/setup-ruby@v1
27
28
  with:
data/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  Starting with v2.0.0, all notable changes to this project will be documented in this file.
4
4
  This project adheres to [Semantic Versioning](http://semver.org/).
5
5
 
6
+ ## v5.0.0 - 2023-05-07
7
+
8
+ * Ruby 3.2 support.
9
+ * LRU caching for HTTP response caching.
10
+ * Raise a ModuleNotFound error instead of just nil when a module is not found.
11
+
6
12
  ## v4.1.0 - 2023-02-21
7
13
 
8
14
  * Add upload method functionality.
data/CODEOWNERS CHANGED
@@ -1 +1 @@
1
- * @puppetlabs/forge-team
1
+ * @puppetlabs/forge-team @bastelfreak
@@ -0,0 +1,78 @@
1
+ require 'digest'
2
+
3
+ module PuppetForge
4
+ # Implements a simple LRU cache. This is used internally by the
5
+ # {PuppetForge::V3::Base} class to cache API responses.
6
+ class LruCache
7
+ # Takes a list of strings (or objects that respond to #to_s) and
8
+ # returns a SHA256 hash of the strings joined with colons. This is
9
+ # a convenience method for generating cache keys. Cache keys do not
10
+ # have to be SHA256 hashes, but they must be unique.
11
+ def self.new_key(*string_args)
12
+ Digest::SHA256.hexdigest(string_args.map(&:to_s).join(':'))
13
+ end
14
+
15
+ # @return [Integer] the maximum number of items to cache.
16
+ attr_reader :max_size
17
+
18
+ # @param max_size [Integer] the maximum number of items to cache. This can
19
+ # be overridden by setting the PUPPET_FORGE_MAX_CACHE_SIZE environment
20
+ # variable.
21
+ def initialize(max_size = 30)
22
+ raise ArgumentError, "max_size must be a positive integer" unless max_size.is_a?(Integer) && max_size > 0
23
+
24
+ @max_size = ENV['PUPPET_FORGE_MAX_CACHE_SIZE'] ? ENV['PUPPET_FORGE_MAX_CACHE_SIZE'].to_i : max_size
25
+ @cache = {}
26
+ @lru = []
27
+ @semaphore = Mutex.new
28
+ end
29
+
30
+ # Retrieves a value from the cache.
31
+ # @param key [Object] the key to look up in the cache
32
+ # @return [Object] the cached value for the given key, or nil if
33
+ # the key is not present in the cache.
34
+ def get(key)
35
+ if cache.key?(key)
36
+ semaphore.synchronize do
37
+ # If the key is present, move it to the front of the LRU
38
+ # list.
39
+ lru.delete(key)
40
+ lru.unshift(key)
41
+ end
42
+ cache[key]
43
+ end
44
+ end
45
+
46
+ # Adds a value to the cache.
47
+ # @param key [Object] the key to add to the cache
48
+ # @param value [Object] the value to add to the cache
49
+ def put(key, value)
50
+ semaphore.synchronize do
51
+ if cache.key?(key)
52
+ # If the key is already present, delete it from the LRU list.
53
+ lru.delete(key)
54
+ elsif cache.size >= max_size
55
+ # If the cache is full, remove the least recently used item.
56
+ cache.delete(lru.pop)
57
+ end
58
+ # Add the key to the front of the LRU list and add the value
59
+ # to the cache.
60
+ lru.unshift(key)
61
+ cache[key] = value
62
+ end
63
+ end
64
+
65
+ # Clears the cache.
66
+ def clear
67
+ semaphore.synchronize do
68
+ cache.clear
69
+ lru.clear
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ # Makes testing easier as these can be accessed directly with #send.
76
+ attr_reader :cache, :lru, :semaphore
77
+ end
78
+ end
@@ -4,6 +4,7 @@ require 'puppet_forge/error'
4
4
 
5
5
  require 'puppet_forge/lazy_accessors'
6
6
  require 'puppet_forge/lazy_relations'
7
+ require 'puppet_forge/lru_cache'
7
8
 
8
9
  module PuppetForge
9
10
  module V3
@@ -53,8 +54,22 @@ module PuppetForge
53
54
  API_VERSION
54
55
  end
55
56
 
57
+ # @private
58
+ def lru_cache
59
+ @lru_cache ||= PuppetForge::LruCache.new
60
+ end
61
+
62
+ # @private
63
+ def lru_cache_key(*args)
64
+ PuppetForge::LruCache.new_key(*args)
65
+ end
66
+
56
67
  # @private
57
68
  def request(resource, item = nil, params = {}, reset_connection = false, conn_opts = {})
69
+ cache_key = lru_cache_key(resource, item, params)
70
+ cached = lru_cache.get(cache_key)
71
+ return cached unless cached.nil?
72
+
58
73
  conn(reset_connection, conn_opts) if reset_connection
59
74
  unless conn.url_prefix.to_s =~ /^#{PuppetForge.host}/
60
75
  conn.url_prefix = "#{PuppetForge.host}"
@@ -69,7 +84,9 @@ module PuppetForge
69
84
  # The API expects a space separated string. This allows the user to invoke it with a more natural feeling array.
70
85
  params['endorsements'] = params['endorsements'].join(' ') if params['endorsements'].is_a? Array
71
86
 
72
- PuppetForge::V3::Base.conn.get uri_path, params
87
+ result = PuppetForge::V3::Base.conn.get uri_path, params
88
+ lru_cache.put(cache_key, result)
89
+ result
73
90
  end
74
91
 
75
92
  # @private
@@ -11,6 +11,11 @@ module PuppetForge
11
11
  lazy :current_release, 'Release'
12
12
  lazy_collection :releases, 'Release'
13
13
 
14
+ def self.find(slug)
15
+ super
16
+ rescue Faraday::ResourceNotFound
17
+ raise PuppetForge::ModuleNotFound, "Module #{slug} not found"
18
+ end
14
19
  end
15
20
  end
16
21
  end
@@ -1,3 +1,3 @@
1
1
  module PuppetForge
2
- VERSION = '4.1.0' # Library version
2
+ VERSION = '5.0.0' # Library version
3
3
  end
data/spec/spec_helper.rb CHANGED
@@ -12,7 +12,10 @@ require 'puppet_forge'
12
12
 
13
13
  module StubbingFaraday
14
14
 
15
- def stub_api_for(klass, base_url = "http://api.example.com")
15
+ def stub_api_for(klass, base_url = "http://api.example.com", lru_cache: false)
16
+ unless lru_cache # Disable LRU cache by default
17
+ allow(klass).to receive(:lru_cache).and_return(instance_double('PuppetForge::LruCache', get: nil, put: nil, clear: nil))
18
+ end
16
19
  allow(klass).to receive(:conn) do
17
20
  Faraday.new :url => base_url do |builder|
18
21
  builder.response(:json, :content_type => /\bjson$/, :parser_options => { :symbolize_names => true })
@@ -38,7 +41,7 @@ module StubbingFaraday
38
41
  [ 404 ].tap do |response|
39
42
  local = File.join(PROJECT_ROOT, 'spec', 'fixtures', xplatform_path)
40
43
 
41
- if File.exists?("#{local}.headers") && File.exists?("#{local}.json")
44
+ if File.exist?("#{local}.headers") && File.exist?("#{local}.json")
42
45
  File.open("#{local}.headers") do |file|
43
46
  response[0] = file.readline[/\d{3}/].to_i
44
47
  response[1] = headers = {}
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ describe PuppetForge::LruCache do
4
+ it 'creates a cache key from a list of strings' do
5
+ expect { subject.class.new_key('foo', 'bar', 'baz') }.not_to raise_error
6
+ end
7
+
8
+ it 'creates a new instance' do
9
+ expect { PuppetForge::LruCache.new(1) }.not_to raise_error
10
+ end
11
+
12
+ it 'raises an error if max_size is not a positive integer' do
13
+ expect { PuppetForge::LruCache.new(-1) }.to raise_error(ArgumentError)
14
+ expect { PuppetForge::LruCache.new(0) }.to raise_error(ArgumentError)
15
+ expect { PuppetForge::LruCache.new(1.5) }.to raise_error(ArgumentError)
16
+ end
17
+
18
+ it 'defaults to a max_size of 30' do
19
+ expect(PuppetForge::LruCache.new.max_size).to eq(30)
20
+ end
21
+
22
+ it 'allows max_size to be set via the max_size parameter' do
23
+ expect(PuppetForge::LruCache.new(42).max_size).to eq(42)
24
+ end
25
+
26
+ it 'provides a #get method' do
27
+ expect(PuppetForge::LruCache.new).to respond_to(:get)
28
+ end
29
+
30
+ it 'provides a #put method' do
31
+ expect(PuppetForge::LruCache.new).to respond_to(:put)
32
+ end
33
+
34
+ it 'provides a #clear method' do
35
+ expect(PuppetForge::LruCache.new).to respond_to(:clear)
36
+ end
37
+
38
+ context 'with environment variables' do
39
+ around(:each) do |example|
40
+ @old_max_size = ENV['PUPPET_FORGE_MAX_CACHE_SIZE']
41
+ ENV['PUPPET_FORGE_MAX_CACHE_SIZE'] = '42'
42
+ example.run
43
+ ENV['PUPPET_FORGE_MAX_CACHE_SIZE'] = @old_max_size
44
+ end
45
+
46
+ it 'uses the value of the PUPPET_FORGE_MAX_CACHE_SIZE environment variable if present' do
47
+ expect(PuppetForge::LruCache.new.max_size).to eq(42)
48
+ end
49
+ end
50
+
51
+ context '#get' do
52
+ it 'returns nil if the key is not present in the cache' do
53
+ expect(PuppetForge::LruCache.new.get('foo')).to be_nil
54
+ end
55
+
56
+ it 'returns the cached value for the given key' do
57
+ cache = PuppetForge::LruCache.new
58
+ cache.put('foo', 'bar')
59
+ expect(cache.get('foo')).to eq('bar')
60
+ end
61
+
62
+ it 'moves the key to the front of the LRU list' do
63
+ cache = PuppetForge::LruCache.new
64
+ cache.put('foo', 'bar')
65
+ cache.put('baz', 'qux')
66
+ cache.get('foo')
67
+ expect(cache.send(:lru)).to eq(['foo', 'baz'])
68
+ end
69
+
70
+ # The below test is non-deterministic but I'm not sure how to unit
71
+ # test thread-safety.
72
+ # it 'is thread-safe' do
73
+ # cache = PuppetForge::LruCache.new
74
+ # cache.put('foo', 'bar')
75
+ # cache.put('baz', 'qux')
76
+ # threads = []
77
+ # threads << Thread.new { 100.times { cache.get('foo') } }
78
+ # threads << Thread.new { 100.times { cache.get('baz') } }
79
+ # threads.each(&:join)
80
+ # expect(cache.send(:lru)).to eq(['baz', 'foo'])
81
+ # end
82
+ end
83
+
84
+ context '#put' do
85
+ it 'adds the key to the front of the LRU list' do
86
+ cache = PuppetForge::LruCache.new
87
+ cache.put('foo', 'bar')
88
+ expect(cache.send(:lru)).to eq(['foo'])
89
+ end
90
+
91
+ it 'adds the value to the cache' do
92
+ cache = PuppetForge::LruCache.new
93
+ cache.put('foo', 'bar')
94
+ expect(cache.send(:cache)).to eq({ 'foo' => 'bar' })
95
+ end
96
+
97
+ it 'removes the least recently used item if the cache is full' do
98
+ cache = PuppetForge::LruCache.new(2)
99
+ cache.put('foo', 'bar')
100
+ cache.put('baz', 'qux')
101
+ cache.put('quux', 'corge')
102
+ expect(cache.send(:lru)).to eq(['quux', 'baz'])
103
+ end
104
+
105
+ # The below test is non-deterministic but I'm not sure how to unit
106
+ # test thread-safety.
107
+ # it 'is thread-safe' do
108
+ # cache = PuppetForge::LruCache.new
109
+ # threads = []
110
+ # threads << Thread.new { 100.times { cache.put('foo', 'bar') } }
111
+ # threads << Thread.new { 100.times { cache.put('baz', 'qux') } }
112
+ # threads.each(&:join)
113
+ # expect(cache.send(:lru)).to eq(['baz', 'foo'])
114
+ # end
115
+ end
116
+
117
+ context '#clear' do
118
+ it 'clears the cache' do
119
+ cache = PuppetForge::LruCache.new
120
+ cache.put('foo', 'bar')
121
+ cache.put('baz', 'qux')
122
+ cache.clear
123
+ expect(cache.send(:lru).empty?).to be_truthy
124
+ expect(cache.send(:cache).empty?).to be_truthy
125
+ end
126
+ end
127
+ end
@@ -61,6 +61,7 @@ describe PuppetForge::V3::Base do
61
61
  describe 'the host url setting' do
62
62
  context 'without a path prefix' do
63
63
  before(:each) do
64
+ PuppetForge::V3::Base.lru_cache.clear # We test the cache later, so clear it now
64
65
  @orig_host = PuppetForge.host
65
66
  PuppetForge.host = 'https://api.example.com'
66
67
 
@@ -83,10 +84,25 @@ describe PuppetForge::V3::Base do
83
84
  base = PuppetForge::V3::Base.find 'puppet'
84
85
  expect(base.username).to eq('foo')
85
86
  end
87
+
88
+ it 'caches responses' do
89
+ stub_api_for(PuppetForge::V3::Base, lru_cache: true) do |stubs|
90
+ stub_fixture(stubs, :get, '/v3/bases/puppet')
91
+ end
92
+ allow(PuppetForge::V3::Base.lru_cache).to receive(:put).and_call_original
93
+ allow(PuppetForge::V3::Base.lru_cache).to receive(:get).and_call_original
94
+
95
+ PuppetForge::V3::Base.find 'puppet'
96
+ PuppetForge::V3::Base.find 'puppet'
97
+ PuppetForge::V3::Base.find 'puppet'
98
+ expect(PuppetForge::V3::Base.lru_cache).to have_received(:put).once
99
+ expect(PuppetForge::V3::Base.lru_cache).to have_received(:get).exactly(3).times
100
+ end
86
101
  end
87
102
 
88
103
  context 'with a path prefix' do
89
104
  before(:each) do
105
+ PuppetForge::V3::Base.lru_cache.clear # We test the cache later, so clear it now
90
106
  @orig_host = PuppetForge.host
91
107
  PuppetForge.host = 'https://api.example.com/uri/prefix'
92
108
 
@@ -109,6 +125,20 @@ describe PuppetForge::V3::Base do
109
125
  base = PuppetForge::V3::Base.find 'puppet'
110
126
  expect(base.username).to eq('bar')
111
127
  end
128
+
129
+ it 'caches responses' do
130
+ stub_api_for(PuppetForge::V3::Base, PuppetForge.host, lru_cache: true) do |stubs|
131
+ stub_fixture(stubs, :get, '/uri/prefix/v3/bases/puppet')
132
+ end
133
+ allow(PuppetForge::V3::Base.lru_cache).to receive(:put).and_call_original
134
+ allow(PuppetForge::V3::Base.lru_cache).to receive(:get).and_call_original
135
+
136
+ PuppetForge::V3::Base.find 'puppet'
137
+ PuppetForge::V3::Base.find 'puppet'
138
+ PuppetForge::V3::Base.find 'puppet'
139
+ expect(PuppetForge::V3::Base.lru_cache).to have_received(:put).once
140
+ expect(PuppetForge::V3::Base.lru_cache).to have_received(:get).exactly(3).times
141
+ end
112
142
  end
113
143
  end
114
144
  end
@@ -28,8 +28,8 @@ describe PuppetForge::V3::Module do
28
28
  expect(mod_stateless.name).to eq('apache')
29
29
  end
30
30
 
31
- it 'returns nil for non-existent modules' do
32
- expect { missing_mod }.to raise_error(Faraday::ResourceNotFound)
31
+ it 'raises exception for non-existent modules' do
32
+ expect { missing_mod }.to raise_error(PuppetForge::ModuleNotFound, 'Module absent-apache not found')
33
33
  end
34
34
  end
35
35
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppet_forge
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet Labs
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-21 00:00:00.000000000 Z
11
+ date: 2023-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -187,6 +187,7 @@ files:
187
187
  - lib/puppet_forge/error.rb
188
188
  - lib/puppet_forge/lazy_accessors.rb
189
189
  - lib/puppet_forge/lazy_relations.rb
190
+ - lib/puppet_forge/lru_cache.rb
190
191
  - lib/puppet_forge/tar.rb
191
192
  - lib/puppet_forge/tar/mini.rb
192
193
  - lib/puppet_forge/unpacker.rb
@@ -236,6 +237,7 @@ files:
236
237
  - spec/unit/forge/connection_spec.rb
237
238
  - spec/unit/forge/lazy_accessors_spec.rb
238
239
  - spec/unit/forge/lazy_relations_spec.rb
240
+ - spec/unit/forge/lru_cache_spec.rb
239
241
  - spec/unit/forge/tar/mini_spec.rb
240
242
  - spec/unit/forge/tar_spec.rb
241
243
  - spec/unit/forge/unpacker_spec.rb
@@ -251,7 +253,7 @@ homepage: https://github.com/puppetlabs/forge-ruby
251
253
  licenses:
252
254
  - Apache-2.0
253
255
  metadata: {}
254
- post_install_message:
256
+ post_install_message:
255
257
  rdoc_options: []
256
258
  require_paths:
257
259
  - lib
@@ -266,8 +268,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
266
268
  - !ruby/object:Gem::Version
267
269
  version: '0'
268
270
  requirements: []
269
- rubygems_version: 3.3.7
270
- signing_key:
271
+ rubygems_version: 3.1.6
272
+ signing_key:
271
273
  specification_version: 4
272
274
  summary: Access the Puppet Forge API from Ruby for resource information and to download
273
275
  releases.
@@ -308,6 +310,7 @@ test_files:
308
310
  - spec/unit/forge/connection_spec.rb
309
311
  - spec/unit/forge/lazy_accessors_spec.rb
310
312
  - spec/unit/forge/lazy_relations_spec.rb
313
+ - spec/unit/forge/lru_cache_spec.rb
311
314
  - spec/unit/forge/tar/mini_spec.rb
312
315
  - spec/unit/forge/tar_spec.rb
313
316
  - spec/unit/forge/unpacker_spec.rb