cutting_edge 0.0.1

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,46 @@
1
+ require 'victor'
2
+
3
+ class Badge
4
+ BADGE_OPTIONS = {
5
+ up_to_date: {
6
+ bg_color: '#32CD32',
7
+ text: 'up-to-date',
8
+ width: 180
9
+ },
10
+ out_of_date: {
11
+ bg_color: '#ff0000',
12
+ text: 'out-of-date',
13
+ width: 190
14
+ },
15
+ unknown: {
16
+ bg_color: '#666',
17
+ text: 'unknown',
18
+ width: 170
19
+ }
20
+ }
21
+
22
+ def self.build_badge(status, num=nil)
23
+ if ! [:up_to_date, :out_of_date, :unknown].include?(status)
24
+ status = :unknown
25
+ end
26
+ number = Integer(num) rescue nil if num
27
+
28
+ svg = Victor::SVG.new width: BADGE_OPTIONS[status][:width], height: 32, template: :html
29
+
30
+ style = {
31
+ stroke: '#d3d3d3',
32
+ stroke_width: 4
33
+ }
34
+
35
+ svg.build do
36
+ rect x: 0, y: 0, width: BADGE_OPTIONS[status][:width], height: 32, fill: BADGE_OPTIONS[status][:bg_color], style: style
37
+
38
+ g font_size: 14, font_family: 'arial', fill: 'white' do
39
+ text "#{number} Dependencies #{BADGE_OPTIONS[status][:text]}", x: 10, y: 20
40
+ end
41
+ end
42
+ return svg.render
43
+ end
44
+
45
+ end
46
+
@@ -0,0 +1,123 @@
1
+ require 'ostruct'
2
+ require 'toml-rb'
3
+
4
+ class Gem::Dependency
5
+ TYPES = [:runtime, :development, :build]
6
+ end
7
+
8
+ module LanguageHelpers
9
+ # Return a mock construct that mimicks Gem::Dependency for depedencies we tried to parse, but weren't valid.
10
+ def unknown_dependency(name, type = :runtime)
11
+ OpenStruct.new(name: name, type: type, requirement: 'unknown')
12
+ end
13
+
14
+ # For each dependency, find its latest version and return the two together. Takes account of invalid or dependencies (see #unknown_dependency)
15
+ #
16
+ # results - Array of Gem::Dependencies and unknown dependencies.
17
+ #
18
+ # Returns an Array of tuples of each dependency and its latest version: e.g., [[<Gem::Dependency>, <Gem::Version>]]
19
+ def dependency_with_latest(results)
20
+ results.map do |dependency|
21
+ [dependency, dependency.requirement.to_s == 'unknown' ? nil : latest_version(dependency.name)]
22
+ end
23
+ end
24
+
25
+ def log_error(message)
26
+ logger.error(message) if ::CuttingEdge::App.enable_logging
27
+ end
28
+ end
29
+
30
+ class Language
31
+ include ::SemanticLogger::Loggable
32
+ extend LanguageHelpers
33
+ end
34
+
35
+ module LanguageVersionHelpers
36
+
37
+ private
38
+
39
+ def canonical_version(version)
40
+ version.match(/^\./) ? "0#{version}" : version
41
+ end
42
+
43
+ # Translate wildcard (*) requirement to Ruby/Gem compatible requirement
44
+ #
45
+ # req - String version requirement
46
+ #
47
+ # Returns a translated String version requirement
48
+ def translate_wildcard(req)
49
+ if req =~ /!=/ # Negative Wildcard
50
+ # Turn != 1.1.* into >= 1.2, < 1.1
51
+ req.sub!('.*', '.0')
52
+ req.sub!('!=', '')
53
+ begin
54
+ v = Gem::Version.new(req) # Create the bumped version using Gem::Version so pre-release handling will work
55
+ rescue ArgumentError => e
56
+ return nil
57
+ end
58
+ lower_bound = ">= #{v.bump.version}"
59
+ upper_bound = "< #{v.version}"
60
+ [lower_bound, upper_bound]
61
+ elsif req =~ /^\s*\*\s*/
62
+ '>= 0' # Turn * into >= 0
63
+ else
64
+ "~> #{req.sub('.*', '.0').sub('=', '')}" # Turn =1.1.* or 1.1.* into ~> 1.1.0
65
+ end
66
+ end
67
+
68
+ # Translate SemVer Caret requirement to Ruby/Gem compatible requirement
69
+ # Caret requirement:
70
+ # lower bound = the unmodified version
71
+ # upper bound = take the left most non-zero digit and +1 it
72
+ #
73
+ # req - String version requirement
74
+ #
75
+ # Returns a translated String version requirement
76
+ def translate_caret(req)
77
+ req.sub!('^', '')
78
+ begin
79
+ version = Gem::Version.new(req)
80
+ rescue ArgumentError => e
81
+ return nil
82
+ end
83
+ segments = version.version.split('.')
84
+ index = segments.find_index {|seg| seg.to_i > 0} # Find the leftmost non-zero digit.
85
+ index = segments.rindex {|seg| seg.to_i == 0} unless index # If there is none, find the last 0.
86
+ segments[index] = segments[index].to_i + 1
87
+ upper_bound = segments[0..index].join('.')
88
+ [">= #{version.version}", "< #{upper_bound}"]
89
+ end
90
+
91
+ def parse_toml(content, sections)
92
+ begin
93
+ config = TomlRB.parse(content)
94
+ rescue TomlRB::ParseError => e
95
+ log_error("Encountered error when parsing TOML: #{e.class} #{e.message}")
96
+ return []
97
+ end
98
+ results = []
99
+
100
+ sections.each do |dependency_type, section_name|
101
+ packages = config[section_name] || next
102
+ packages.each do |name, info|
103
+ requirement = info.fetch('version', nil) rescue info
104
+ if requirement
105
+ requirements = requirement.split(',').map {|req| translate_requirement(req)}
106
+ next if requirements.include?(nil) # If a sub-requirement failed to translate, skip this entire dependency.
107
+ begin
108
+ results << Gem::Dependency.new(name, requirements, dependency_type)
109
+ rescue StandardError => e
110
+ log_error("Encountered error when parsing requirement #{requirements}: #{e.class} #{e.message}")
111
+ next
112
+ end
113
+ else
114
+ results << unknown_dependency(name, dependency_type)
115
+ end
116
+ end
117
+ end
118
+ results
119
+ end
120
+
121
+ end
122
+
123
+ Dir[File.expand_path('../langs/*.rb', __FILE__)].each { |f| require f }
@@ -0,0 +1,102 @@
1
+ require 'rubygems'
2
+ require 'http'
3
+
4
+ class PythonLang < Language
5
+ # For Requirements.txt
6
+ # See https://iscompatible.readthedocs.io/en/latest/
7
+ COMPARATORS = />=|>|<=|<|==/
8
+ VERSION_NUM = /\d[\.\w]*/
9
+ SUFFIX_OPTION = /\s*(\[.*\])?/
10
+ NAME = /[^,]+/
11
+ REGEX = /^(#{NAME})\s*(#{COMPARATORS})\s*(#{VERSION_NUM})(\s*,\s*(#{COMPARATORS})\s*(#{VERSION_NUM}))?#{SUFFIX_OPTION}$/
12
+
13
+ API_URL = 'https://pypi.org/pypi/'
14
+
15
+ PIPFILE_SECTIONS = {
16
+ :runtime => 'packages',
17
+ :development => 'dev-packages'
18
+ }
19
+
20
+ extend LanguageVersionHelpers
21
+
22
+ class << self
23
+
24
+ # Defaults for projects in this language
25
+ def locations(name = nil)
26
+ ['requirements.txt', 'Pipfile']
27
+ end
28
+
29
+ # Parse a dependency file
30
+ #
31
+ # name - String contents of the file
32
+ # content - String contents of the file
33
+ #
34
+ # Returns an Array of tuples of each dependency and its latest version: [[<Gem::Dependency>, <Gem::Version>]]
35
+ def parse_file(name, content)
36
+ return nil unless content
37
+ if name =~ /\.txt$/
38
+ results = parse_requirements(content)
39
+ elsif name =~ /Pipfile/
40
+ results = parse_toml(content, PIPFILE_SECTIONS)
41
+ end
42
+ dependency_with_latest(results) if results
43
+ end
44
+
45
+ def parse_requirements(content)
46
+ results = []
47
+ content.each_line do |line|
48
+ next if line =~ /^\s*-e/ # ignore 'editable' dependencies
49
+ if line =~ COMPARATORS
50
+ next unless match = line.match(REGEX) # Skip this line if it doesn't conform to our expectations
51
+ name, first_comp, first_version, _ignore, second_comp, second_version = match.captures
52
+ first_comp = '=' if first_comp == '=='
53
+ second_comp = '=' if second_comp == '=='
54
+ dep = Gem::Dependency.new(name, "#{first_comp} #{first_version}")
55
+ dep.requirement.concat(["#{second_comp} #{second_version}"]) if second_comp && second_version
56
+ else
57
+ dep = Gem::Dependency.new(line.strip) # requries version to be >= 0
58
+ end
59
+ results << dep
60
+ end
61
+ results
62
+ end
63
+
64
+ # Find the latest versions of a dependency by name
65
+ #
66
+ # name - String name of the dependency
67
+ #
68
+ # Returns a Gem::Version
69
+ def latest_version(name)
70
+ begin
71
+ content = HTTP.timeout(::CuttingEdge::LAST_VERSION_TIMEOUT).follow(max_hops: 1).get(::File.join(API_URL, name, 'json')).parse
72
+ version = content['info']['version']
73
+ Gem::Version.new(canonical_version(version))
74
+ rescue StandardError, HTTP::TimeoutError => e
75
+ log_error("Encountered error when fetching latest version of #{name}: #{e.class} #{e.message}")
76
+ nil
77
+ end
78
+ end
79
+
80
+ # Translate version requirement syntax for Pipfiles to a String or Array of Strings that Gem::Dependency.new understands
81
+ # Pipfile support * and != requirements, which Ruby does not
82
+ # See https://www.python.org/dev/peps/pep-0440/#version-matching
83
+ #
84
+ # req - String version requirement
85
+ #
86
+ # Returns a translated String version requirement
87
+ def translate_requirement(req)
88
+ req.sub!('~=', '~>')
89
+ req.sub!('==', '=')
90
+ case req
91
+ when /\*/
92
+ translate_wildcard(req)
93
+ when '!='
94
+ req.sub!('!=', '')
95
+ ["< #{req}", "> #{req}"]
96
+ else
97
+ req
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,47 @@
1
+ require 'gemnasium/parser'
2
+ require 'rubygems'
3
+
4
+ class RubyLang < Language
5
+
6
+ class << self
7
+
8
+ # Defaults for projects in this language
9
+ def locations(name)
10
+ ["#{name}.gemspec", 'Gemfile']
11
+ end
12
+
13
+ # Parse a dependency file
14
+ #
15
+ # name - String contents of the file
16
+ # content - String contents of the file
17
+ #
18
+ # Returns an Array of tuples of each dependency and its latest version: [[<Bundler::Dependency>, <Gem::Version>]]
19
+ def parse_file(name, content)
20
+ return nil unless content
21
+ results = name =~ /gemspec/ ? parse_gemspec(content) : parse_gemfile(content)
22
+ dependency_with_latest(results)
23
+ end
24
+
25
+ def latest_version(name)
26
+ # Fancy todo: cache these?
27
+ begin
28
+ Gem::SpecFetcher.fetcher.spec_for_dependency(Gem::Dependency.new(name, nil)).flatten.first.version
29
+ rescue StandardError => e
30
+ log_error("Encountered error when fetching latest version of #{name}: #{e.class} #{e.message}")
31
+ nil
32
+ end
33
+ end
34
+
35
+ def parse_ruby(type, content)
36
+ Gemnasium::Parser.send(type, content).dependencies
37
+ end
38
+
39
+ def parse_gemspec(content)
40
+ parse_ruby(:gemspec, content)
41
+ end
42
+
43
+ def parse_gemfile(content)
44
+ parse_ruby(:gemfile, content)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,68 @@
1
+ require 'rubygems'
2
+ require 'http'
3
+
4
+ class RustLang < Language
5
+ API_URL = 'https://crates.io/api/v1/crates'
6
+
7
+ CARGOFILE_SECTIONS = {
8
+ :runtime => 'dependencies',
9
+ :development => 'dev-dependencies',
10
+ :build => 'build-dependencies'
11
+ }
12
+
13
+ extend LanguageVersionHelpers
14
+
15
+ class << self
16
+
17
+ # Defaults for projects in this language
18
+ def locations(name = nil)
19
+ ['Cargo.toml']
20
+ end
21
+
22
+ # Parse a dependency file
23
+ #
24
+ # name - String contents of the file
25
+ # content - String contents of the file
26
+ #
27
+ # Returns an Array of tuples of each dependency and its latest version: [[<Gem::Dependency>, <Gem::Version>]]
28
+ def parse_file(name, content)
29
+ return nil unless content
30
+ results = parse_toml(content, CARGOFILE_SECTIONS)
31
+ dependency_with_latest(results) if results
32
+ end
33
+
34
+ # Find the latest versions of a dependency by name
35
+ #
36
+ # name - String name of the dependency
37
+ #
38
+ # Returns a Gem::Version
39
+ def latest_version(name)
40
+ begin
41
+ content = HTTP.timeout(::CuttingEdge::LAST_VERSION_TIMEOUT).get(::File.join(API_URL, name)).parse
42
+ version = content['crate']['max_version']
43
+ Gem::Version.new(canonical_version(version))
44
+ rescue StandardError, HTTP::Error => e
45
+ log_error("Encountered error when fetching latest version of #{name}: #{e.class} #{e.message}")
46
+ nil
47
+ end
48
+ end
49
+
50
+ # Translate Cargo version requirement syntax to a String or Array of Strings that Gem::Dependency.new understands
51
+ # Cargo.toml files support * and ^ (wildcard and caret) requirements, which Ruby does not
52
+ # See: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
53
+ #
54
+ # req - String version requirement
55
+ #
56
+ # Returns a translated String version requirement
57
+ def translate_requirement(req)
58
+ if req =~ /~|<|>|\*|=/
59
+ return translate_wildcard(req) if req =~ /\*/
60
+ req.sub!('~', '~>')
61
+ req
62
+ else
63
+ translate_caret(req)
64
+ end
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,67 @@
1
+ require File.expand_path('../langs.rb', __FILE__)
2
+
3
+ module CuttingEdge
4
+ class Repository
5
+
6
+ DEPENDENCY_TYPES = [:runtime] # Which dependency types to accept (default only :runtime, excludes :development).
7
+ DEFAULT_LANG = 'ruby'
8
+
9
+ attr_reader :token, :locations, :lang
10
+ attr_accessor :dependency_types
11
+
12
+ def initialize(org, name, lang = nil, locations = nil, branch = nil, token = nil)
13
+ @org = org
14
+ @name = name
15
+ @branch = branch || 'master'
16
+ @token = token
17
+ @lang = lang || DEFAULT_LANG
18
+ @locations = {}
19
+ (locations || get_lang(@lang).locations(name)).each do |loc|
20
+ @locations[loc] = url_for_file(loc)
21
+ end
22
+ @dependency_types = DEPENDENCY_TYPES
23
+ end
24
+
25
+ def source
26
+ ''
27
+ end
28
+
29
+ def identifier
30
+ File.join(source, @org, @name)
31
+ end
32
+
33
+ def url_for_file(file)
34
+ file
35
+ end
36
+
37
+ private
38
+
39
+ def get_lang(lang)
40
+ Object.const_get("::#{lang.capitalize}Lang")
41
+ end
42
+ end
43
+
44
+ class GithubRepository < Repository
45
+ HOST = 'https://raw.githubusercontent.com'
46
+
47
+ def source
48
+ 'github'
49
+ end
50
+
51
+ def url_for_file(file)
52
+ File.join(HOST, @org, @name, @branch, file)
53
+ end
54
+ end
55
+
56
+ class GitlabRepository < Repository
57
+ HOST = 'https://gitlab.com/'
58
+
59
+ def source
60
+ 'gitlab'
61
+ end
62
+
63
+ def url_for_file(file)
64
+ File.join(HOST, @org, @name, 'raw', @branch, file)
65
+ end
66
+ end
67
+ end