puppet_forge 4.1.0 → 5.0.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: 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