Class: MyPrecious::RubyGemInfo

Inherits:
Object
  • Object
show all
Includes:
DataCaching
Defined in:
lib/myprecious/ruby_gems.rb

Constant Summary collapse

MIN_RELEASED_DAYS =
90
MIN_STABLE_DAYS =
14
INFO_CACHE_DIR =
MyPrecious.data_cache(DATA_DIR / 'rb-info-cache')
VERSIONS_CACHE_DIR =
MyPrecious.data_cache(DATA_DIR / 'rb-versions-cache')
SOURCE_CODE_URI_ENTRIES =
%w[github_repo source_code_uri]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DataCaching

print_error_info

Constructor Details

#initialize(name) ⇒ RubyGemInfo

Returns a new instance of RubyGemInfo.



94
95
96
97
98
# File 'lib/myprecious/ruby_gems.rb', line 94

def initialize(name)
  super()
  @name = name
  @version_reqs = Gem::Requirement.new
end

Instance Attribute Details

#current_versionObject

Returns the value of attribute current_version.



100
101
102
# File 'lib/myprecious/ruby_gems.rb', line 100

def current_version
  @current_version
end

#nameObject (readonly)

Returns the value of attribute name.



99
100
101
# File 'lib/myprecious/ruby_gems.rb', line 99

def name
  @name
end

#version_reqsObject (readonly)

Returns the value of attribute version_reqs.



99
100
101
# File 'lib/myprecious/ruby_gems.rb', line 99

def version_reqs
  @version_reqs
end

Class Method Details

.accum_gem_lock_info(fpath, **opts) ⇒ Object

Build a Hash mapping names of gems used by a project to RubyGemInfo about them

The project at fpath must have a “Gemfile.lock” file as used by the bundler gem.

The accumulated RubyGemInfo instances should have non-nil #current_version values and meaningful information in #version_reqs, as indicated in the Gemfile.lock for fpath.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/myprecious/ruby_gems.rb', line 69

def self.accum_gem_lock_info(fpath, **opts)
  {}.tap do |gems|
    each_gem_used(fpath, **opts) do |entry_type, name, verreq|
      g = (gems[name] ||= RubyGemInfo.new(name))
      
      case entry_type
      when :current
        g.current_version = verreq
      when :reqs
        g.version_reqs.concat verreq.as_list
      end
    end
  end
end

.col_title(attr) ⇒ Object

Get an appropriate, human friendly column title for an attribute



87
88
89
90
91
92
# File 'lib/myprecious/ruby_gems.rb', line 87

def self.col_title(attr)
  case attr
  when :name then 'Gem'
  else Reporting.common_col_title(attr)
  end
end

.each_gem_used(fpath, gemfile: 'Gemfile') ⇒ Object

Enumerate Ruby gems used in a project

The project at fpath must have a “Gemfile.lock” file as used by the bundler gem.

The block receives an Array with three values:

  • Either :current or :reqs, indicating the meaning of the element at index 2,

  • The name of the gem, and

  • Either a Gem::Version (if the index-0 element is :current) or a Gem::Requirement (if the index-0 element is :reqs)

Iterations yielding :current given the version of the gem currently specified by the Gemfile.lock in the project. Iterations yielding :reqs give requirements on the specified gem dictated by other gems used by the project. Each gem name will appear in only one :current iteration, but may occur in multiple :reqs iterations.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/myprecious/ruby_gems.rb', line 38

def self.each_gem_used(fpath, gemfile: 'Gemfile')
  return enum_for(:each_gem_used, fpath) unless block_given?
  
  gemlock = Pathname(fpath).join(gemfile + '.lock')
  raise "No #{gemfile}.lock in #{fpath}" unless gemlock.exist?
  
  section = nil
  gemlock.each_line do |l|
    break if l.upcase == l && section == 'GEM'
    
    case l
    when /^[A-Z]*\s*$/
      section = l.strip
    when /^\s*(?<gem>\S+)\s+\(\s*(?<gemver>\d[^)]*)\)/
      yield [:current, $~[:gem], Gem::Version.new($~[:gemver])] if section == 'GEM'
    when /^\s*(?<gem>\S+)\s+\(\s*(?<verreqs>[^)]*)\)/
      yield [:reqs, $~[:gem], Gem::Requirement.new(*$~[:verreqs].split(/,\s*/))] if section == 'GEM'
    end
  end
end

Instance Method Details

#ageObject

Age in days of the current version



167
168
169
170
# File 'lib/myprecious/ruby_gems.rb', line 167

def age
  return @age if defined? @age
  @age = get_age
end

#changelogObject



221
222
223
# File 'lib/myprecious/ruby_gems.rb', line 221

def changelog
  changelogs[0]
end

#changelogsObject



213
214
215
216
217
218
219
# File 'lib/myprecious/ruby_gems.rb', line 213

def changelogs
  gv_data = get_gems_versions.sort_by {|v| Gem::Version.new(v['number'])}.reverse
  if current_version
    gv_data = gv_data.take_while {|v| Gem::Version.new(v['number']) > current_version}
  end
  gv_data.collect {|v| (v['metadata'] || {})['changelog_uri']}.compact.uniq
end

#cvesObject



207
208
209
210
211
# File 'lib/myprecious/ruby_gems.rb', line 207

def cves
  CVEs.get_for(name, current_version && current_version.to_s).map do |cve, appl|
    cve
  end
end


235
236
237
238
239
240
241
# File 'lib/myprecious/ruby_gems.rb', line 235

def days_between_current_and_recommended
  v, cv_rel = versions_with_release.find {|v, r| v == current_version} || []
  v, rv_rel = versions_with_release.find {|v, r| v == recommended_version} || []
  return nil if cv_rel.nil? || rv_rel.nil?
  
  return ((rv_rel - cv_rel) / ONE_DAY).to_i
end

#homepage_uriObject



106
107
108
# File 'lib/myprecious/ruby_gems.rb', line 106

def homepage_uri
  get_gems_info['homepage_uri']
end

#inspectObject



102
103
104
# File 'lib/myprecious/ruby_gems.rb', line 102

def inspect
  %Q{#<#{self.class.name}:#{'%#.8x' % (object_id << 1)} "#{name}">}
end

#latest_releasedObject



159
160
161
162
# File 'lib/myprecious/ruby_gems.rb', line 159

def latest_released
  return nil if versions_with_release.empty?
  Date.parse(versions_with_release[0][1].to_s).to_s
end

#latest_versionObject



154
155
156
157
# File 'lib/myprecious/ruby_gems.rb', line 154

def latest_version
  return nil if versions_with_release.empty?
  versions_with_release[0][0]
end

#licenseObject



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/myprecious/ruby_gems.rb', line 172

def license
  gv_data = get_gems_versions
  
  curver_data = gv_data.find {|v| Gem::Version.new(v['number']) == current_version}
  current_licenses = curver_data && curver_data['licenses'] || []
  
  rcmdd_data = gv_data.find {|v| Gem::Version.new(v['number']) == recommended_version}
  rcmdd_licenses = rcmdd_data && rcmdd_data['licenses'] || current_licenses
  
  now_included = rcmdd_licenses - current_licenses
  now_excluded = current_licenses - rcmdd_licenses
  
  case 
  when now_included.empty? && now_excluded.empty?
    LicenseDescription.new(current_licenses.join(' or '))
  when !now_excluded.empty?
    # "#{current_licenses.join(' or ')} (but rec'd ver. doesn't allow #{now_excluded.join(' or ')})"
    LicenseDescription.new(current_licenses.join(' or ')).tap do |desc|
      desc.update_info = "rec'd ver. doesn't allow #{now_excluded.join(' or ')}"
    end
  when current_licenses.empty? && !now_included.empty?
    LicenseDescription.new("Rec'd ver.: #{now_included.join(' or ')}")
  when !now_included.empty?
    # "#{current_licenses.join(' or ')} (or #{now_included.join(' or ')} on upgrade to rec'd ver.)"
    LicenseDescription.new(current_licenses.join(' or ')).tap do |desc|
      desc.update_info = "or #{now_included.join(' or ')} on upgrade to rec'd ver."
    end
  else
    # "#{current_licenses.join(' or ')} (rec'd ver.: #{rcmdd_licenses.join(' or ')})"
    LicenseDescription.new(current_licenses.join(' or ')).tap do |desc|
      desc.update_info = "rec'd ver.: #{rcmdd_licenses.join(' or ')}"
    end
  end
end

#obsolescenceObject



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/myprecious/ruby_gems.rb', line 243

def obsolescence
  cv_major = current_version && current_version.segments[0]
  rv_major = recommended_version && recommended_version.segments[0]
  at_least_moderate = false
  case 
  when cv_major.nil? || rv_major.nil?
    # Can't compare
  when cv_major + 1 < rv_major
    # More than a single major version difference is severe
    return :severe
  when cv_major < rv_major
    # Moderate obsolescence if we're a major version behind
    at_least_moderate = true
  end
  
  days_between = days_between_current_and_recommended
  
  return Reporting.obsolescence_by_age(days_between, at_least_moderate: at_least_moderate)
end

Version number recommended based on stability criteria

May return nil if no version meets the established criteria



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/myprecious/ruby_gems.rb', line 129

def recommended_version
  return nil if versions_with_release.empty?
  return @recommended_version if defined? @recommended_version
  
  orig_time_horizon = time_horizon = \
    Time.now - (MIN_RELEASED_DAYS * ONE_DAY)
  horizon_versegs = nonpatch_versegs(versions_with_release[0][0])
  
  versions_with_release.each do |ver, released|
    next if ver.prerelease?
    return (@recommended_version = current_version) if current_version && current_version >= ver
    
    # Reset the time-horizon clock if moving back into previous patch-series
    if (nonpatch_versegs(ver) <=> horizon_versegs) < 0
      time_horizon = orig_time_horizon
    end
    
    if released < time_horizon && version_reqs.satisfied_by?(ver)
      return (@recommended_version = ver)
    end
    time_horizon = [time_horizon, released - (MIN_STABLE_DAYS * ONE_DAY)].min
  end
  return (@recommended_version = nil)
end

#release_history_urlObject



225
226
227
228
229
230
231
232
233
# File 'lib/myprecious/ruby_gems.rb', line 225

def release_history_url
  "https://rubygems.org/gems/#{name}/versions" if (
    begin
      get_gems_versions
    rescue StandardError
      nil
    end
  )
end

#source_code_uriObject



263
264
265
266
267
# File 'lib/myprecious/ruby_gems.rb', line 263

def source_code_uri
   = get_gems_info['metadata']
  SOURCE_CODE_URI_ENTRIES.each {|k| return [k] if [k]}
  return nil
end

#versions_with_releaseObject

An Array of Arrays containing version (Gem::Version) and release date (Time)

The returned Array is sorted in order of descending version number.



115
116
117
118
119
120
121
122
# File 'lib/myprecious/ruby_gems.rb', line 115

def versions_with_release
  @versions ||= get_gems_versions.map do |ver|
    [
      Gem::Version.new(ver['number']),
      Time.parse(ver['created_at']).freeze
    ].freeze
  end.reject {|vn, rd| vn.prerelease?}.sort.reverse.freeze
end