gem_lookup 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|