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.
@@ -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
@@ -4,7 +4,8 @@ module GemsBond
4
4
  # Makes Rails aware of the tasks
5
5
  class Railtie < Rails::Railtie
6
6
  rake_tasks do
7
- load "tasks/gems_bond/spy.rake"
7
+ load "tasks/gems_bond/spy/one.rake"
8
+ load "tasks/gems_bond/spy/all.rake"
8
9
  end
9
10
  end
10
11
  end
@@ -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