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.



92
93
94
95
96
# File 'lib/myprecious/ruby_gems.rb', line 92

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

Instance Attribute Details

#current_versionObject

Returns the value of attribute current_version.



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

def current_version
  @current_version
end

#nameObject (readonly)

Returns the value of attribute name.



97
98
99
# File 'lib/myprecious/ruby_gems.rb', line 97

def name
  @name
end

#version_reqsObject (readonly)

Returns the value of attribute version_reqs.



97
98
99
# File 'lib/myprecious/ruby_gems.rb', line 97

def version_reqs
  @version_reqs
end

Class Method Details

.accum_gem_lock_info(fpath) ⇒ 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.



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

def self.accum_gem_lock_info(fpath)
  {}.tap do |gems|
    each_gem_used(fpath) 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



85
86
87
88
89
90
# File 'lib/myprecious/ruby_gems.rb', line 85

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

.each_gem_used(fpath) ⇒ 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.



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

def self.each_gem_used(fpath)
  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



165
166
167
168
# File 'lib/myprecious/ruby_gems.rb', line 165

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

#changelogObject



213
214
215
# File 'lib/myprecious/ruby_gems.rb', line 213

def changelog
  changelogs[0]
end

#changelogsObject



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

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


217
218
219
220
221
222
223
# File 'lib/myprecious/ruby_gems.rb', line 217

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



104
105
106
# File 'lib/myprecious/ruby_gems.rb', line 104

def homepage_uri
  get_gems_info['homepage_uri']
end

#inspectObject



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

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

#latest_releasedObject



157
158
159
160
# File 'lib/myprecious/ruby_gems.rb', line 157

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

#latest_versionObject



152
153
154
155
# File 'lib/myprecious/ruby_gems.rb', line 152

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

#licenseObject



170
171
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
# File 'lib/myprecious/ruby_gems.rb', line 170

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



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/myprecious/ruby_gems.rb', line 225

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



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

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

#source_code_uriObject



245
246
247
248
249
# File 'lib/myprecious/ruby_gems.rb', line 245

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.



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

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