gem_lookup 1.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 +7 -0
- data/.github/workflows/tests.yml +16 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +46 -0
- data/CHANGELOG.md +98 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +24 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +378 -0
- data/Rakefile +12 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/booster_pack.rb +5 -0
- data/exe/gems +8 -0
- data/gem_lookup.gemspec +42 -0
- data/init/bundler.rb +6 -0
- data/init/environment.rb +3 -0
- data/init/zeitwerk.rb +13 -0
- data/lib/gem_lookup.rb +3 -0
- data/lib/gem_lookup/errors.rb +13 -0
- data/lib/gem_lookup/flags.rb +49 -0
- data/lib/gem_lookup/gems.rb +75 -0
- data/lib/gem_lookup/help.rb +78 -0
- data/lib/gem_lookup/rate_limit.rb +34 -0
- data/lib/gem_lookup/requests.rb +69 -0
- data/lib/gem_lookup/ruby_gems.rb +132 -0
- data/lib/gem_lookup/serializers/emoji.rb +120 -0
- data/lib/gem_lookup/serializers/interface.rb +51 -0
- data/lib/gem_lookup/serializers/json.rb +23 -0
- data/lib/gem_lookup/serializers/wordy.rb +118 -0
- data/lib/gem_lookup/version.rb +9 -0
- metadata +125 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
ENV['APP_ENV'] = 'development'
|
5
|
+
require_relative '../booster_pack'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
Pry.start
|
data/bin/setup
ADDED
data/booster_pack.rb
ADDED
data/exe/gems
ADDED
data/gem_lookup.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/gem_lookup/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'gem_lookup'
|
7
|
+
spec.version = GemLookup::VERSION
|
8
|
+
spec.authors = ['Josh Mills']
|
9
|
+
spec.email = ['josh@trueheart78.com']
|
10
|
+
|
11
|
+
spec.summary = 'Retrieves gem-related information from https://rubygems.org'
|
12
|
+
spec.description = <<~DESC
|
13
|
+
Simple but effective command line interface that looks up gems using RubyGems.org's public API
|
14
|
+
and displays results in an emoji-filled fashion.
|
15
|
+
DESC
|
16
|
+
spec.homepage = 'https://github.com/trueheart78/gem_lookup'
|
17
|
+
spec.license = 'MIT'
|
18
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
|
19
|
+
|
20
|
+
spec.metadata = {
|
21
|
+
'bug_tracker_uri' => 'https://github.com/trueheart78/gem_lookup/issues',
|
22
|
+
'changelog_uri' => 'https://github.com/trueheart78/gem_lookup/blob/main/CHANGELOG.md',
|
23
|
+
'documentation_uri' => spec.homepage,
|
24
|
+
'homepage_uri' => spec.homepage,
|
25
|
+
'source_code_uri' => "https://github.com/trueheart78/gem_lookup/tree/v#{GemLookup::VERSION}"
|
26
|
+
}
|
27
|
+
|
28
|
+
# Specify which files should be added to the gem when it is released.
|
29
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
30
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
31
|
+
`git ls-files -z`.split("\x0").reject {|f| f.match(%r{\A(?:test|spec|features)/}) }
|
32
|
+
end
|
33
|
+
spec.bindir = 'exe'
|
34
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) {|f| File.basename(f) }
|
35
|
+
|
36
|
+
# These gems need to be explicitly required to be utilized.
|
37
|
+
spec.add_runtime_dependency 'colorize', '~> 0.8.1'
|
38
|
+
spec.add_runtime_dependency 'typhoeus', '~> 1.4'
|
39
|
+
spec.add_runtime_dependency 'zeitwerk', '~> 2.4.2'
|
40
|
+
|
41
|
+
# Development and test dependencies are defined in the Gemfile due to ENV['APP_ENV'] usage.
|
42
|
+
end
|
data/init/bundler.rb
ADDED
data/init/environment.rb
ADDED
data/init/zeitwerk.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zeitwerk'
|
4
|
+
|
5
|
+
loader = Zeitwerk::Loader.new
|
6
|
+
|
7
|
+
root_dir = File.expand_path(File.dirname(File.dirname(__FILE__)))
|
8
|
+
|
9
|
+
loader.push_dir(File.join(root_dir, 'lib'))
|
10
|
+
|
11
|
+
loader.push_dir(File.join(root_dir, 'spec'))
|
12
|
+
loader.ignore(File.join(root_dir, '**', '*_spec.rb'))
|
13
|
+
loader.setup
|
data/lib/gem_lookup.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GemLookup
|
4
|
+
module Errors
|
5
|
+
class InvalidDisplayMode < StandardError; end
|
6
|
+
|
7
|
+
class UndefinedInterfaceMethod < StandardError; end
|
8
|
+
|
9
|
+
class UnsupportedFlags < StandardError; end
|
10
|
+
|
11
|
+
class UnsupportedFlag < StandardError; end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colorize'
|
4
|
+
|
5
|
+
module GemLookup
|
6
|
+
class Flags
|
7
|
+
class << self
|
8
|
+
# Returns the supported flags.
|
9
|
+
# @return [Hash] the supported flags.
|
10
|
+
def supported
|
11
|
+
{
|
12
|
+
help: { matches: %w[-h --help], desc: 'Display the help screen.' },
|
13
|
+
version: { matches: %w[-v --version], desc: 'Display version information.' },
|
14
|
+
json: { matches: %w[-j --json], desc: 'Bulk the output results as raw JSON.' },
|
15
|
+
wordy: { matches: %w[-w --wordy], desc: 'Stream the output using only words.' }
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
# Checks to see if any flags passed in match those defined by the type.
|
20
|
+
# @param type [Symbol] the type of flag (`:help`, `:version`, etc).
|
21
|
+
# @param flags [Array] an array that may or may not contain a supported flag.
|
22
|
+
# @return [Boolean] whether the flags passed in contain a flag supported by type.
|
23
|
+
def supported?(type, flags: [])
|
24
|
+
return false if type.nil?
|
25
|
+
return false if flags.empty?
|
26
|
+
|
27
|
+
type = type.to_sym
|
28
|
+
return false unless supported.key? type
|
29
|
+
|
30
|
+
supported[type][:matches].each do |flag|
|
31
|
+
return true if flags.include? flag
|
32
|
+
end
|
33
|
+
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
# Outputs the unsupported flags and exits with a code of 1.
|
38
|
+
# @param flags [Array] the list of unsupported flags.
|
39
|
+
def unsupported(flags:)
|
40
|
+
flags = flags.compact.reject(&:empty?)
|
41
|
+
return false unless flags.any?
|
42
|
+
|
43
|
+
raise Errors::UnsupportedFlag, flags.first if flags.size == 1
|
44
|
+
|
45
|
+
raise Errors::UnsupportedFlags, flags.join(', ')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module GemLookup
|
6
|
+
class Gems
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :@serializer, :display, :batch_iterator, :gem_count, :querying, :streaming?
|
10
|
+
|
11
|
+
def initialize(gem_list, serializer:)
|
12
|
+
@gem_list = gem_list
|
13
|
+
@serializer = serializer
|
14
|
+
@batches = []
|
15
|
+
@json = { gems: [] }
|
16
|
+
end
|
17
|
+
|
18
|
+
def process
|
19
|
+
batch_gems
|
20
|
+
process_batches
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def process_batches
|
26
|
+
display_gem_count
|
27
|
+
|
28
|
+
@batches.each_with_index {|batch, index| process_batch batch: batch, index: index }
|
29
|
+
|
30
|
+
display(json: @json) unless streaming?
|
31
|
+
end
|
32
|
+
|
33
|
+
def process_batch(batch:, index:)
|
34
|
+
display_batch_details(index + 1, @batches.size, batch)
|
35
|
+
|
36
|
+
make_requests batch: batch
|
37
|
+
display_results
|
38
|
+
|
39
|
+
sleep RateLimit.interval if batch_mode?
|
40
|
+
end
|
41
|
+
|
42
|
+
def make_requests(batch:)
|
43
|
+
@json = Requests.new(batch: batch, json: @json).process
|
44
|
+
end
|
45
|
+
|
46
|
+
def batch_gems
|
47
|
+
gems = @gem_list.dup
|
48
|
+
|
49
|
+
@batches.push gems.shift(RateLimit.number) while gems.any?
|
50
|
+
end
|
51
|
+
|
52
|
+
def batch_mode?
|
53
|
+
@batches.size > 1
|
54
|
+
end
|
55
|
+
|
56
|
+
def display_gem_count
|
57
|
+
return unless streaming?
|
58
|
+
|
59
|
+
gem_count(num: @gem_list.size) if @gem_list.size > 1
|
60
|
+
end
|
61
|
+
|
62
|
+
def display_batch_details(num, total, batch)
|
63
|
+
return unless streaming?
|
64
|
+
|
65
|
+
batch_iterator(num: num, total: total) if batch_mode?
|
66
|
+
querying(batch: batch)
|
67
|
+
end
|
68
|
+
|
69
|
+
def display_results
|
70
|
+
return unless streaming?
|
71
|
+
|
72
|
+
display(json: @json[:gems].shift) while @json[:gems].any?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GemLookup
|
4
|
+
class Help
|
5
|
+
# @return [Numeric] the spacing for output options
|
6
|
+
OUTPUT_OPTION_SPACING = 21
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Outputs the generated content.
|
10
|
+
# @param exit_code [Numeric, nil] the exit code (if any) to exit with.
|
11
|
+
def display(exit_code: nil)
|
12
|
+
puts documentation
|
13
|
+
|
14
|
+
exit exit_code unless exit_code.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
# Outputs the gem name and current gem version.
|
18
|
+
# @param exit_code [Numeric, nil] the exit code (if any) to exit with.
|
19
|
+
def version(exit_code: nil)
|
20
|
+
puts "#{NAME} #{VERSION}"
|
21
|
+
|
22
|
+
exit exit_code unless exit_code.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
# Generates the help documentation.
|
26
|
+
# @return [String] the help documentation.
|
27
|
+
def documentation
|
28
|
+
<<~HELP
|
29
|
+
|
30
|
+
Usage: gems [OPTIONS] GEMS
|
31
|
+
|
32
|
+
Retrieves gem-related information from https://rubygems.org
|
33
|
+
|
34
|
+
Example: gems -j rails rspec
|
35
|
+
|
36
|
+
This application's purpose is to make working with with RubyGems.org easier. 💖
|
37
|
+
It uses the RubyGems public API to perform lookups, and parses the JSON response
|
38
|
+
body to provide details about the most recent version, as well as links to
|
39
|
+
the home page, source code, changelog, and mailing list.
|
40
|
+
|
41
|
+
Feel free to pass in as many gems that you like, as it makes requests in
|
42
|
+
parallel. There is a rate limit, #{RateLimit.number}/sec. If it detects the amount of gems it
|
43
|
+
has been passed is more than the rate limit, the application will run in Batch
|
44
|
+
mode, and introduce a one second delay between batch lookups.
|
45
|
+
|
46
|
+
#{options}
|
47
|
+
|
48
|
+
Rate limit documentation: #{RateLimit.documentation_url}
|
49
|
+
HELP
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Generates an Output Options string that includes the supported flag details.
|
55
|
+
# @return [String] the supported output options.
|
56
|
+
def options
|
57
|
+
<<~OPTIONS.chomp
|
58
|
+
Output Options:
|
59
|
+
#{flag_output}
|
60
|
+
OPTIONS
|
61
|
+
end
|
62
|
+
|
63
|
+
# rubocop:disable Metrics/AbcSize
|
64
|
+
# Generates a formatted string that displays the supported flag details.
|
65
|
+
# @return [String] the supported flags and their details.
|
66
|
+
def flag_output
|
67
|
+
[].tap do |output|
|
68
|
+
Flags.supported.keys.sort.each do |key|
|
69
|
+
matches = Flags.supported[key][:matches].join ' '
|
70
|
+
spaces = ' ' * (OUTPUT_OPTION_SPACING - matches.length)
|
71
|
+
output.push " #{matches}#{spaces}#{Flags.supported[key][:desc]}"
|
72
|
+
end
|
73
|
+
end.join "\n"
|
74
|
+
end
|
75
|
+
# rubocop:enable Metrics/AbcSize
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GemLookup
|
4
|
+
module RateLimit
|
5
|
+
# @return [Numeric] the max requests that can be made per interval.
|
6
|
+
MAX_REQUESTS_PER_INTERVAL = 10
|
7
|
+
|
8
|
+
# @return [Numeric] the frequency interval, in seconds.
|
9
|
+
FREQUENCY_INTERVAL_IN_SECONDS = 1
|
10
|
+
|
11
|
+
# @return [String] the url that documents the rate limit
|
12
|
+
RATE_LIMIT_DOCUMENTATION_URL = 'https://guides.rubygems.org/rubygems-org-rate-limits/'
|
13
|
+
|
14
|
+
class << self
|
15
|
+
# Calls MAX_REQUESTS_PER_SECOND.
|
16
|
+
# @return [Numeric] the max requests that can be made per interval.
|
17
|
+
def number
|
18
|
+
MAX_REQUESTS_PER_INTERVAL
|
19
|
+
end
|
20
|
+
|
21
|
+
# Calls FREQUENCY_INTERVAL_IN_SECONDS
|
22
|
+
# @return [Numeric] the frequency interval, in seconds.
|
23
|
+
def interval
|
24
|
+
FREQUENCY_INTERVAL_IN_SECONDS
|
25
|
+
end
|
26
|
+
|
27
|
+
# Calls RATE_LIMIT_DOCUMENTATION_URL.
|
28
|
+
# @return [String] the url that documents the rate limit.
|
29
|
+
def documentation_url
|
30
|
+
RATE_LIMIT_DOCUMENTATION_URL
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'typhoeus'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module GemLookup
|
7
|
+
class Requests
|
8
|
+
# @return [Numeric] the seconds to wait before a response is considered timed out.
|
9
|
+
TIMEOUT_THRESHOLD = 10
|
10
|
+
|
11
|
+
def initialize(batch:, json: nil)
|
12
|
+
@batch = batch
|
13
|
+
@json = json || { gems: [] }
|
14
|
+
end
|
15
|
+
|
16
|
+
def process
|
17
|
+
Typhoeus::Hydra.hydra.tap do |hydra|
|
18
|
+
populate_requests hydra: hydra, batch: @batch
|
19
|
+
end.run
|
20
|
+
|
21
|
+
@json
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def populate_requests(hydra:, batch:)
|
27
|
+
batch.each do |gem_name|
|
28
|
+
hydra.queue build_request gem_name: gem_name
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# rubocop:disable Layout/LineLength
|
33
|
+
def build_request(gem_name:)
|
34
|
+
url = api_url gem_name: gem_name
|
35
|
+
Typhoeus::Request.new(url, accept_encoding: 'gzip', timeout: TIMEOUT_THRESHOLD).tap do |request|
|
36
|
+
request.on_complete do |response|
|
37
|
+
if response.success?
|
38
|
+
handle_successful_response json: JSON.parse(response.body, symbolize_names: true)
|
39
|
+
elsif response.timed_out?
|
40
|
+
handle_timed_out_response gem_name: gem_name
|
41
|
+
else
|
42
|
+
handle_failed_response gem_name: gem_name
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
# rubocop:enable Layout/LineLength
|
48
|
+
|
49
|
+
def handle_successful_response(json:)
|
50
|
+
json[:exists] = true
|
51
|
+
json[:timeout] = false
|
52
|
+
@json[:gems].push json
|
53
|
+
end
|
54
|
+
|
55
|
+
def handle_timed_out_response(gem_name:)
|
56
|
+
json = { name: gem_name, exists: false, timeout: true }
|
57
|
+
@json[:gems].push json
|
58
|
+
end
|
59
|
+
|
60
|
+
def handle_failed_response(gem_name:)
|
61
|
+
json = { name: gem_name, exists: false, timeout: false }
|
62
|
+
@json[:gems].push json
|
63
|
+
end
|
64
|
+
|
65
|
+
def api_url(gem_name:)
|
66
|
+
"https://rubygems.org/api/v1/gems/#{gem_name}.json"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|