gems_bond 1.0.4 → 1.1.0

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