Skip to content

Commit

Permalink
Update Geminabox Dependency Fetching with Adapter Pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
numbata committed Apr 14, 2024
1 parent a044c44 commit 1f9008b
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 91 deletions.
5 changes: 5 additions & 0 deletions lib/geminabox.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def self.geminabox_path(file)
autoload :Server, geminabox_path('server')
autoload :DiskCache, geminabox_path('disk_cache')
autoload :IncomingGem, geminabox_path('incoming_gem')
autoload :RubygemsAdapter, geminabox_path('rubygems_adapter')

class << self

Expand All @@ -46,13 +47,15 @@ class << self
:gem_permissions,
:allow_delete,
:rubygems_proxy,
:rubygems_adapter,
:rubygems_proxy_merge_strategy,
:http_adapter,
:lockfile,
:retry_interval,
:allow_remote_failure,
:ruby_gems_url,
:bundler_ruby_gems_url,
:index_ruby_gems_url,
:allow_upload,
:on_gem_received
)
Expand Down Expand Up @@ -90,6 +93,7 @@ def call(env)
allow_replace: false,
gem_permissions: 0644,
rubygems_proxy: (ENV['RUBYGEMS_PROXY'] == 'true'),
rubygems_adapter: ENV.fetch('RUBYGEMS_ADAPTER') { :index }.to_sym,
rubygems_proxy_merge_strategy: ENV.fetch('RUBYGEMS_PROXY_MERGE_STRATEGY') { :local_gems_take_precedence_over_remote_gems }.to_sym,
allow_delete: true,
http_adapter: HttpClientAdapter.new,
Expand All @@ -98,6 +102,7 @@ def call(env)
allow_remote_failure: false,
ruby_gems_url: 'https://rubygems.org/',
bundler_ruby_gems_url: 'https://bundler.rubygems.org/',
index_ruby_gems_url: 'https://index.rubygems.org/',
allow_upload: true,
on_gem_received: nil
)
Expand Down
12 changes: 12 additions & 0 deletions lib/geminabox/rubygems_adapter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module Geminabox
module RubygemsAdapter
def self.adapter_path(file)
File.join File.dirname(__FILE__), 'rubygems_adapter', file
end

autoload :IndexApi, adapter_path('index_api')
autoload :DependencyApi, adapter_path('dependency_api')
end
end
27 changes: 27 additions & 0 deletions lib/geminabox/rubygems_adapter/dependency_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

module Geminabox
module RubygemsAdapter
module DependencyApi
class << self
def for(*gems)
url = rubygems_uri.tap do |uri|
uri.query = URI.encode_www_form(gems: gems.map(&:to_s).join(','))
end

body = Geminabox.http_adapter.get_content(url)
Marshal.load(body)
rescue => e
return [] if Geminabox.allow_remote_failure
raise e
end

private

def rubygems_uri
URI.join(Geminabox.bundler_ruby_gems_url, '/api/v1/dependencies')
end
end
end
end
end
52 changes: 52 additions & 0 deletions lib/geminabox/rubygems_adapter/index_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Geminabox
module RubygemsAdapter
module IndexApi
class << self
def for(*gems)
gems.each_with_object([]) do |gem_name, accum|
accum.push(*for_gem(gem_name))
end
rescue => e
return [] if Geminabox.allow_remote_failure
raise e
end

private

def rubygems_gem_uri(gem_name)
URI.join(Geminabox.index_ruby_gems_url, "/info/#{gem_name}")
end

def for_gem(gem_name)
uri = rubygems_gem_uri(gem_name)
response = Geminabox.http_adapter.get_content(uri)
response.each_line.each_with_object([]) do |line, result|
next unless line.include?('|')

version_meta, _metadata = line.split('|')
version_info, dependencies = version_meta.split(' ', 2)

version, platform = version_info.split('-', 2)
platform ||= "ruby"

dependencies = dependencies.split(',').each_with_object([]) do |dep, accum|
name, requirements = dep.split(':')
requirements.split('&').each do |requirement|
accum.push([name.strip, requirement.strip])
end
end

result.push({
"name" => gem_name.to_s,
"number" => version,
"platform" => platform,
"dependencies" => dependencies
})
end
end
end
end
end
end
28 changes: 6 additions & 22 deletions lib/geminabox/rubygems_dependency.rb
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
# frozen_string_literal: true

require 'json'
require 'uri'

module Geminabox
module RubygemsDependency

class << self

def for(*gems)

url = [
rubygems_uri,
'?gems=',
gems.map(&:to_s).join(',')
].join
body = Geminabox.http_adapter.get_content(url)
Marshal.load(body)
rescue Exception => e
return [] if Geminabox.allow_remote_failure
raise e
case Geminabox.rubygems_adapter.to_sym
when :dependency_api, :dependency
Geminabox::RubygemsAdapter::DependencyApi.for(*gems)
else
Geminabox::RubygemsAdapter::IndexApi.for(*gems)
end
end

def rubygems_uri
URI.join(Geminabox.bundler_ruby_gems_url, '/api/v1/dependencies')
end

end
end
end

99 changes: 99 additions & 0 deletions test/units/geminabox/rubygems_adapter/dependency_api_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
require_relative '../../../test_helper'

module Geminabox
module RubygemsAdapter
class DependencyApiTest < Minitest::Test
def setup
Geminabox.rubygems_adapter = :dependency_api
end

def teardown
Geminabox.rubygems_adapter = :index
Geminabox.http_adapter = HttpClientAdapter.new
Geminabox.allow_remote_failure = false
end

def test_get_list
stub_request(:get, "https://bundler.rubygems.org/api/v1/dependencies?gems=some_gem,other_gem").
to_return(:status => 200, :body => Marshal.dump(some_gem_dependencies), :headers => {"Content-Type" => 'application/octet-stream'})

assert_equal some_gem_dependencies, RubygemsDependency.for(:some_gem, :other_gem)
end

def test_get_list_with_500_error
stub_request(:get, "https://bundler.rubygems.org/api/v1/dependencies?gems=some_gem,other_gem").
to_return(:status => 500, :body => 'Whoops')

assert_raises HTTPClient::BadResponseError do
RubygemsDependency.for(:some_gem, :other_gem)
end
end

def test_get_list_with_401_error
stub_request(:get, "https://bundler.rubygems.org/api/v1/dependencies?gems=some_gem,other_gem").
to_return(:status => 401, :body => 'Whoops')
assert_raises HTTPClient::BadResponseError do
RubygemsDependency.for(:some_gem, :other_gem)
end
end

def test_get_list_with_socket_error
http_adapter = HttpSocketErrorDummy.new
http_adapter.default_response = 'getaddrinfo: Name or service not known'
Geminabox.http_adapter = http_adapter
assert_raises SocketError do
RubygemsDependency.for(:some_gem, :other_gem)
end
end

def test_get_list_with_500_error_and_allow_remote_failure
stub_request(:get, "https://bundler.rubygems.org/api/v1/dependencies?gems=some_gem,other_gem").
to_return(:status => 500, :body => 'Whoops')

Geminabox.allow_remote_failure = true
assert_equal [], RubygemsDependency.for(:some_gem, :other_gem)
end

def test_get_list_with_401_error_and_allow_remote_failure
stub_request(:get, "https://bundler.rubygems.org/api/v1/dependencies?gems=some_gem,other_gem").
to_return(:status => 401, :body => 'Whoops')

Geminabox.allow_remote_failure = true
assert_equal [], RubygemsDependency.for(:some_gem, :other_gem)
end

def test_get_list_with_socket_error_and_allow_remote_failure
http_adapter = HttpSocketErrorDummy.new
http_adapter.default_response = 'getaddrinfo: Name or service not known'
Geminabox.http_adapter = http_adapter
Geminabox.allow_remote_failure = true
assert_equal [], RubygemsDependency.for(:some_gem, :other_gem)
end

def some_gem_dependencies
[
{
'name' => 'some_gem',
'number' => '0.0.1',
'platform' => 'ruby',
'dependencies' => []
},
{
'name' => 'some_gem',
'number' => '0.0.2',
'platform' => 'ruby',
'dependencies' => []
},
{
'name' => 'other_gem',
'number' => '0.0.1',
'platform' => 'ruby',
'dependencies' => [
['some_gem', ">= 0"]
]
}
]
end
end
end
end
108 changes: 108 additions & 0 deletions test/units/geminabox/rubygems_adapter/index_api_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
require_relative '../../../test_helper'

module Geminabox
module RubygemsAdapter
class IndexApiTest < Minitest::Test
def teardown
Geminabox.http_adapter = HttpClientAdapter.new
Geminabox.allow_remote_failure = false
end

def test_get_list
stub_request(:get, "https://index.rubygems.org/info/some_gem").
to_return(:status => 200, :body => gem_info[:some_gem], :headers => {"Content-Type" => 'application/octet-stream'})
stub_request(:get, "https://index.rubygems.org/info/other_gem").
to_return(:status => 200, :body => gem_info[:other_gem], :headers => {"Content-Type" => 'application/octet-stream'})

assert_equal some_gem_dependencies, RubygemsDependency.for(:some_gem, :other_gem)
end

def test_get_list_with_500_error
stub_request(:get, "https://index.rubygems.org/info/some_gem").
to_return(:status => 500, :body => 'Whoops')

assert_raises HTTPClient::BadResponseError do
RubygemsDependency.for(:some_gem, :other_gem)
end
end

def test_get_list_with_401_error
stub_request(:get, "https://index.rubygems.org/info/some_gem").
to_return(:status => 401, :body => 'Whoops')
assert_raises HTTPClient::BadResponseError do
RubygemsDependency.for(:some_gem, :other_gem)
end
end

def test_get_list_with_socket_error
http_adapter = HttpSocketErrorDummy.new
http_adapter.default_response = 'getaddrinfo: Name or service not known'
Geminabox.http_adapter = http_adapter
assert_raises SocketError do
RubygemsDependency.for(:some_gem, :other_gem)
end
end

def test_get_list_with_500_error_and_allow_remote_failure
stub_request(:get, "https://index.rubygems.org/info/some_gem").
to_return(:status => 500, :body => 'Whoops')

Geminabox.allow_remote_failure = true
assert_equal [], RubygemsDependency.for(:some_gem, :other_gem)
end

def test_get_list_with_401_error_and_allow_remote_failure
stub_request(:get, "https://index.rubygems.org/info/some_gem").
to_return(:status => 401, :body => 'Whoops')

Geminabox.allow_remote_failure = true
assert_equal [], RubygemsDependency.for(:some_gem, :other_gem)
end

def test_get_list_with_socket_error_and_allow_remote_failure
http_adapter = HttpSocketErrorDummy.new
http_adapter.default_response = 'getaddrinfo: Name or service not known'
Geminabox.http_adapter = http_adapter
Geminabox.allow_remote_failure = true
assert_equal [], RubygemsDependency.for(:some_gem, :other_gem)
end

def gem_info
{
some_gem: <<~GEM_INFO,
0.0.1 | checksum:foo
0.0.2 | checksum:foo
GEM_INFO
other_gem: <<~GEM_INFO
0.0.1 some_gem:>= 0| checksum:foo
GEM_INFO
}
end

def some_gem_dependencies
[
{
'name' => 'some_gem',
'number' => '0.0.1',
'platform' => 'ruby',
'dependencies' => []
},
{
'name' => 'some_gem',
'number' => '0.0.2',
'platform' => 'ruby',
'dependencies' => []
},
{
'name' => 'other_gem',
'number' => '0.0.1',
'platform' => 'ruby',
'dependencies' => [
['some_gem', ">= 0"]
]
}
]
end
end
end
end
Loading

0 comments on commit 1f9008b

Please sign in to comment.