cutting_edge 0.0.1

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