gems_bond 1.0.4 → 1.1.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 +4 -4
- data/lib/gems_bond.rb +3 -2
- data/lib/gems_bond/configuration.rb +2 -0
- data/lib/gems_bond/{examination_helper.rb → examination.rb} +30 -18
- data/lib/gems_bond/fetchers/fetcher.rb +34 -0
- data/lib/gems_bond/{fetcher → fetchers}/github.rb +54 -10
- data/lib/gems_bond/fetchers/ruby_gems.rb +82 -0
- data/lib/gems_bond/gem.rb +128 -10
- data/lib/gems_bond/helpers/concurrency_helper.rb +24 -0
- data/lib/gems_bond/helpers/formatting_helper.rb +48 -0
- data/lib/gems_bond/printers/csv.rb +54 -0
- data/lib/gems_bond/printers/html.rb +61 -0
- data/lib/gems_bond/printers/printer.rb +58 -0
- data/lib/gems_bond/printers/stdout.rb +90 -0
- data/lib/gems_bond/railtie.rb +2 -1
- data/lib/gems_bond/spy/all.rb +93 -0
- data/lib/gems_bond/spy/one.rb +59 -0
- data/lib/gems_bond/version.rb +1 -1
- data/lib/tasks/gems_bond/spy/all.rake +36 -0
- data/lib/tasks/gems_bond/spy/one.rake +29 -0
- metadata +17 -10
- data/lib/gems_bond/fetch_helper.rb +0 -71
- data/lib/gems_bond/fetcher/ruby_gems.rb +0 -58
- data/lib/gems_bond/printer.rb +0 -88
- data/lib/gems_bond/spy.rb +0 -62
- data/lib/tasks/gems_bond/spy.rake +0 -34
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GemsBond
|
4
|
+
module Helpers
|
5
|
+
# Concurrency helper
|
6
|
+
module ConcurrencyHelper
|
7
|
+
# Run each item concurrently
|
8
|
+
# @param items [Boolean] items to process
|
9
|
+
# @yield [item] apply to each item
|
10
|
+
# @return [void]
|
11
|
+
# @example
|
12
|
+
# each_concurrently(words) do |word|
|
13
|
+
# dictionnary_api.fetch(word)
|
14
|
+
# end
|
15
|
+
def each_concurrently(items)
|
16
|
+
threads = []
|
17
|
+
items.each do |item|
|
18
|
+
threads << Thread.new { block_given? ? yield(item) : item }
|
19
|
+
end
|
20
|
+
threads.each(&:join)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GemsBond
|
4
|
+
module Helpers
|
5
|
+
# Formatting helper
|
6
|
+
module FormattingHelper
|
7
|
+
MISSING = "-"
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# Returns a date with a readable format
|
12
|
+
# @param date [Date]
|
13
|
+
# @return [String]
|
14
|
+
# @example
|
15
|
+
# human_date(Date.new(2017, 11, 19)) #=> "2007-11-19"
|
16
|
+
# human_date(nil) #=> "-"
|
17
|
+
def human_date(date)
|
18
|
+
return MISSING if date.nil?
|
19
|
+
|
20
|
+
date.strftime("%F")
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a number with a readable format
|
24
|
+
# @param date [Integer]
|
25
|
+
# @return [String]
|
26
|
+
# @example
|
27
|
+
# human_number(1_000_000) #=> "1 000 000"
|
28
|
+
# human_number(nil) #=> "-"
|
29
|
+
def human_number(number)
|
30
|
+
return MISSING if number.nil?
|
31
|
+
|
32
|
+
number.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1 ")
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns score out of 100
|
36
|
+
# @param date [Float]
|
37
|
+
# @return [String]
|
38
|
+
# @example
|
39
|
+
# human_score(0.5) #=> "50"
|
40
|
+
# human_score(nil) #=> "-"
|
41
|
+
def human_score(score)
|
42
|
+
return MISSING if score.nil?
|
43
|
+
|
44
|
+
(score * 100).round
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "csv"
|
4
|
+
require "gems_bond/printers/printer"
|
5
|
+
|
6
|
+
module GemsBond
|
7
|
+
module Printers
|
8
|
+
# Prints CSV file
|
9
|
+
class CSV < Printer
|
10
|
+
DATA = %w[
|
11
|
+
name
|
12
|
+
homepage
|
13
|
+
source_code_uri
|
14
|
+
version
|
15
|
+
last_version
|
16
|
+
last_version_date
|
17
|
+
days_since_last_version
|
18
|
+
last_commit_date
|
19
|
+
days_since_last_commit
|
20
|
+
downloads_count
|
21
|
+
contributors_count
|
22
|
+
stars_count
|
23
|
+
forks_count
|
24
|
+
open_issues_count
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Prints data into a CSV file
|
30
|
+
# @return [void]
|
31
|
+
def print
|
32
|
+
::CSV.open("#{DIRECTORY_PATH}/spy.csv", "w") do |csv|
|
33
|
+
csv << headers
|
34
|
+
@gems.each do |gem|
|
35
|
+
csv << row(gem)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Generates CSV headers
|
41
|
+
# @return [Array]
|
42
|
+
def headers
|
43
|
+
DATA
|
44
|
+
end
|
45
|
+
|
46
|
+
# Generates CSV row for a gem
|
47
|
+
# @param gem [GemsBond::Gem]
|
48
|
+
# @return [Array]
|
49
|
+
def row(gem)
|
50
|
+
DATA.map { |data| gem.public_send(data) }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "erb"
|
4
|
+
require "gems_bond/printers/printer"
|
5
|
+
require "gems_bond/helpers/formatting_helper"
|
6
|
+
|
7
|
+
module GemsBond
|
8
|
+
module Printers
|
9
|
+
# Prints HTML file
|
10
|
+
class HTML < Printer
|
11
|
+
include GemsBond::Helpers::FormattingHelper
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# Prints data into a file
|
16
|
+
# @return [void]
|
17
|
+
def print
|
18
|
+
File.open("#{DIRECTORY_PATH}/spy.html", "w") do |file|
|
19
|
+
file.puts content
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the ERB template
|
24
|
+
# @return [String]
|
25
|
+
def template
|
26
|
+
File.read(File.join(File.dirname(__FILE__), "../../../views/", "diagnosis.erb"))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the HTML content
|
30
|
+
# @return [String]
|
31
|
+
def content
|
32
|
+
ERB.new(template).result(binding)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a color depending on the gap to last released version
|
36
|
+
# @param gap [Integer]
|
37
|
+
# @return [String] branding for Bootstrap use
|
38
|
+
def version_color(gap)
|
39
|
+
return "secondary" if gap.nil?
|
40
|
+
return "success" if gap.zero?
|
41
|
+
|
42
|
+
gap < 3 ? "warning" : "danger"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns a color depending on the score value
|
46
|
+
# @param score [Float] in [0, 1]
|
47
|
+
# @return [String] branding for Bootstrap use
|
48
|
+
def color(score)
|
49
|
+
return "secondary" if score.nil?
|
50
|
+
|
51
|
+
if score < 0.33
|
52
|
+
"danger"
|
53
|
+
elsif score < 0.66
|
54
|
+
"warning"
|
55
|
+
else
|
56
|
+
"success"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module GemsBond
|
6
|
+
module Printers
|
7
|
+
# Prints HTML file
|
8
|
+
class Printer
|
9
|
+
DIRECTORY_PATH = "gems_bond"
|
10
|
+
|
11
|
+
# Initializes an instance
|
12
|
+
# @param gems [Array<GemsBond::Gem>]
|
13
|
+
# @return [GemsBond::Printers]
|
14
|
+
def initialize(gems)
|
15
|
+
@gems = gems
|
16
|
+
end
|
17
|
+
|
18
|
+
# Manages print
|
19
|
+
# @return [void]
|
20
|
+
def call
|
21
|
+
format = self.class.name.split("::").last
|
22
|
+
puts "\nPreparing data for printing results into #{format} file..."
|
23
|
+
create_directory
|
24
|
+
print
|
25
|
+
puts "Open file gems_bond/spy.#{format.downcase} to display the results."
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Prints data into a file
|
31
|
+
# @return [void]
|
32
|
+
def print
|
33
|
+
raise NotImplementedError, "Subclasses must implement a call method."
|
34
|
+
end
|
35
|
+
|
36
|
+
# Creates the "gems_bond" directory if absent
|
37
|
+
def create_directory
|
38
|
+
return if File.directory?(DIRECTORY_PATH)
|
39
|
+
|
40
|
+
FileUtils.mkdir_p(DIRECTORY_PATH)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns gems sorted by the average_score
|
44
|
+
# @return [Array<GemsBond::Gem>]
|
45
|
+
# @note gems with no average_score are put at the end
|
46
|
+
def sorted_gems
|
47
|
+
# sort with putting gems without average_score at the end
|
48
|
+
@gems.sort do |a, b|
|
49
|
+
if a.average_score && b.average_score
|
50
|
+
a.average_score <=> b.average_score
|
51
|
+
else
|
52
|
+
a.average_score ? -1 : 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "gems_bond/helpers/formatting_helper"
|
4
|
+
|
5
|
+
module GemsBond
|
6
|
+
module Printers
|
7
|
+
# Inspects gem and outputs the result
|
8
|
+
class Stdout < SimpleDelegator
|
9
|
+
include GemsBond::Helpers::FormattingHelper
|
10
|
+
|
11
|
+
# Logs gem information
|
12
|
+
# @return [void]
|
13
|
+
def call
|
14
|
+
line_break
|
15
|
+
puts "--- #{name} info ---".upcase
|
16
|
+
line_break
|
17
|
+
puts description
|
18
|
+
line_break
|
19
|
+
log_url
|
20
|
+
log_version
|
21
|
+
log_counts
|
22
|
+
log_activity
|
23
|
+
line_break
|
24
|
+
return unless unknown
|
25
|
+
|
26
|
+
puts "Note: this gem is not listed in your current dependencies!"
|
27
|
+
line_break
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Adds a line break
|
33
|
+
def line_break
|
34
|
+
puts ""
|
35
|
+
end
|
36
|
+
|
37
|
+
# Logs url information
|
38
|
+
def log_url
|
39
|
+
url = source_code_uri || homepage || "?"
|
40
|
+
puts "- url: #{url}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Logs version information
|
44
|
+
def log_version
|
45
|
+
return unless version_gap
|
46
|
+
|
47
|
+
alert = version_gap.zero? ? "(up-to-date)" : "(#{version_gap} behind #{last_version})"
|
48
|
+
puts "- version: #{version} #{alert}"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Logs counts (downloads, forks, stars, contributors) information
|
52
|
+
def log_counts
|
53
|
+
print "- counts: "
|
54
|
+
content = []
|
55
|
+
add_count =
|
56
|
+
lambda do |type|
|
57
|
+
count = public_send("#{type}_count")
|
58
|
+
return unless count
|
59
|
+
|
60
|
+
content << "#{human_number(count)} #{type}"
|
61
|
+
end
|
62
|
+
|
63
|
+
add_count.call("downloads")
|
64
|
+
add_count.call("forks")
|
65
|
+
add_count.call("stars")
|
66
|
+
add_count.call("contributors")
|
67
|
+
|
68
|
+
puts content.empty? ? "unknown" : content.join(" | ")
|
69
|
+
end
|
70
|
+
|
71
|
+
# Logs activity information
|
72
|
+
def log_activity
|
73
|
+
print "- activity: "
|
74
|
+
content = []
|
75
|
+
add_days_since_last =
|
76
|
+
lambda do |days, type|
|
77
|
+
return if days.nil?
|
78
|
+
|
79
|
+
unit = days.zero? || days > 1 ? "days" : "day"
|
80
|
+
content << "#{human_number(days)} #{unit} since last #{type}"
|
81
|
+
end
|
82
|
+
|
83
|
+
add_days_since_last.call(days_since_last_version, "version")
|
84
|
+
add_days_since_last.call(days_since_last_commit, "commit")
|
85
|
+
|
86
|
+
puts content.empty? ? "unkown" : content.join(" | ")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/gems_bond/railtie.rb
CHANGED
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "gems_bond/gem"
|
4
|
+
require "gems_bond/helpers/concurrency_helper"
|
5
|
+
require "gems_bond/printers/html"
|
6
|
+
require "gems_bond/printers/csv"
|
7
|
+
|
8
|
+
module GemsBond
|
9
|
+
module Spy
|
10
|
+
# Inspects gems and outputs the result in HTML and CSV files
|
11
|
+
class All
|
12
|
+
include GemsBond::Helpers::ConcurrencyHelper
|
13
|
+
|
14
|
+
# Number of fetch retries before skipping gem
|
15
|
+
RETRIES = 2
|
16
|
+
|
17
|
+
# Fetches and scores gems then prints result
|
18
|
+
# @return [void]
|
19
|
+
def call
|
20
|
+
timer do
|
21
|
+
fetch_gems_data
|
22
|
+
GemsBond::Printers::HTML.new(gems).call
|
23
|
+
GemsBond::Printers::CSV.new(gems).call
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Returns number of gems
|
30
|
+
# @return [Integer]
|
31
|
+
def gems_count
|
32
|
+
@gems_count ||= gems.count
|
33
|
+
end
|
34
|
+
|
35
|
+
# Starts a timer and executes given block
|
36
|
+
# @yieldparam [Proc] code to execute and time
|
37
|
+
# @return [void] (stdout)
|
38
|
+
def timer
|
39
|
+
start_at = Time.now
|
40
|
+
yield
|
41
|
+
seconds = Time.now - start_at
|
42
|
+
time_per_gem_text = "#{(seconds / Float(gems_count)).round(2)} second(s) per gem"
|
43
|
+
puts "\nIt took #{seconds} second(s) to spy #{gems_count} gem(s) (#{time_per_gem_text})."
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns list of gems to spy
|
47
|
+
# @return [Array<GemsBond::Gem>]
|
48
|
+
def gems
|
49
|
+
@gems ||=
|
50
|
+
Bundler.load.current_dependencies.map do |dependency|
|
51
|
+
GemsBond::Gem.new(dependency)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Fetches data for each gem
|
56
|
+
# @return [void] (mutate gems)
|
57
|
+
# @note use concurrency to fetch quickly fetch data from APIs
|
58
|
+
def fetch_gems_data
|
59
|
+
puts "Fetching data for..."
|
60
|
+
# slice 100 to avoid too many requests on RubyGems and GitHub APIs
|
61
|
+
gems.each_slice(100) do |batch|
|
62
|
+
each_concurrently(batch) do |gem|
|
63
|
+
begin
|
64
|
+
retries ||= 0
|
65
|
+
# set verbose to true to stdout the gem name
|
66
|
+
gem.prepare_data(verbose: true)
|
67
|
+
# rescue SocketError, Faraday::ConnectionFailed...
|
68
|
+
rescue StandardError
|
69
|
+
(retries += 1) <= RETRIES ? retry : nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Starts a thread to process the given gem
|
76
|
+
# @param gem [GemsBond::Gem] gem to process
|
77
|
+
# @note if there is a connection/API error
|
78
|
+
# retry or rescue if too many retries
|
79
|
+
def gem_thread(gem)
|
80
|
+
Thread.new do
|
81
|
+
begin
|
82
|
+
retries ||= 0
|
83
|
+
# set verbose to true to stdout the gem name
|
84
|
+
gem.prepare_data(verbose: true)
|
85
|
+
# rescue SocketError, Faraday::ConnectionFailed...
|
86
|
+
rescue StandardError
|
87
|
+
(retries += 1) <= RETRIES ? retry : nil
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|