Class: Kettle::Jem::Appraisals::ActiveRecordSupportGemfileGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb,
sig/kettle/jem/appraisals.rbs

Overview

Generates the modular gemfiles that give ActiveRecord test suites a database driver on every engine: sqlite3 on MRI/TruffleRuby and activerecord-jdbcsqlite3-adapter (plus bigdecimal) on JRuby.

The sqlite3 requirement comes from Kettle::Rb::GemDependencies, which records what each ActiveRecord series' sqlite3 adapter declares. One support gemfile is written per run of consecutive ActiveRecord series sharing a requirement, named after the oldest series in the run (+gemfiles/modular/activerecord_support/v6.0.gemfile+ covers 6.0–7.0). ActiveRecord newer than the recorded series uses the newest run.

The JDBC adapter's release series tracks ActiveRecord (52.x, 61.x, 72.x…), so an open requirement lets Bundler pick the adapter matching the resolved ActiveRecord.

Files that exist without the GENERATED_HEADER are treated as hand-maintained and are never overwritten.

Examples:

generator = ActiveRecordSupportGemfileGenerator.new(base_dir: "/path/to/gem")
generator.generate(["7.1", "8.0"])
generator.eval_path_for("7.1.6") #=> "modular/activerecord_support/v7.1.gemfile"

Constant Summary collapse

GENERATED_HEADER =

Returns header marking a support gemfile as generated (and safe to regenerate).

Returns:

  • (String) —

    header marking a support gemfile as generated (and safe to regenerate)

"# Generated by kettle-jem-appraisals"
SUPPORT_DIR =

Returns directory holding the support gemfiles.

Returns:

  • (String) —

    directory holding the support gemfiles

"gemfiles/modular/activerecord_support"
UNKNOWN_PATH =

Returns support gemfile for ActiveRecord older than every recorded series.

Returns:

  • (String) —

    support gemfile for ActiveRecord older than every recorded series

"#{SUPPORT_DIR}/unknown.gemfile"
LEGACY_PATHS =

Returns support gemfiles written by kettle-jem-appraisals 0.1.4 and earlier.

Returns:

  • (Array<String>) —

    support gemfiles written by kettle-jem-appraisals 0.1.4 and earlier

[
  "gemfiles/modular/activerecord_support.gemfile",
  "gemfiles/modular/activerecord_support_modern.gemfile"
].freeze
LEGACY_PATH =

Returns:

  • (String)
MODERN_PATH =

Returns:

  • (String)
MODERN_ACTIVERECORD =

Returns:

  • (Gem::Version)
VARIANTS =

Returns:

  • (Hash[String, Hash[Symbol, untyped]])

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(base_dir: Dir.pwd) ⇒ ActiveRecordSupportGemfileGenerator

Returns a new instance of ActiveRecordSupportGemfileGenerator.

Parameters:

  • base_dir (String) (defaults to: Dir.pwd) —

    path to the project root

  • base_dir: (String) (defaults to: Dir.pwd)


62
63
64
# File 'lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb', line 62

def initialize(base_dir: Dir.pwd)
  @base_dir = base_dir
end

Instance Attribute Details

#base_dir ⇒ String (readonly)

Returns absolute path to the project root.

Returns:

  • (String) —

    absolute path to the project root



59
60
61
# File 'lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb', line 59

def base_dir
  @base_dir
end

Class Method Details

.support_gemfile?(path) ⇒ Boolean

Returns whether path names an ActiveRecord support gemfile.

Parameters:

  • path (String) —

    a gemfile path, with or without the leading gemfiles/

Returns:

  • (Boolean) —

    whether path names an ActiveRecord support gemfile



50
51
52
53
54
55
# File 'lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb', line 50

def support_gemfile?(path)
  normalized = path.to_s.strip.sub(%r{\Agemfiles/}, "")
  return true if normalized.start_with?("#{SUPPORT_DIR.delete_prefix("gemfiles/")}/")

  LEGACY_PATHS.any? { |legacy| legacy.delete_prefix("gemfiles/") == normalized }
end

Instance Method Details

#build_content(group) ⇒ String

Parameters:

  • variant (Hash[Symbol, untyped])

Returns:

  • (String)


166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb', line 166

def build_content(group)
  requirements = group ? group.first.requirements : []
  sqlite3 = [%("sqlite3"), *requirements.map(&:inspect), "platforms: [:ruby]"].join(", ")
  notes = []
  notes << "# sqlite3 requirement from kettle-rb: #{group.last.source}" if group
  unfixed = group ? unfixed_sqlite3_advisories(group.first.series) : []
  notes << "# kettle-rb: no sqlite3 release allowed here fixes #{unfixed.join(", ")}" unless unfixed.empty?

  "    # frozen_string_literal: true\n\n    \#{GENERATED_HEADER}\n    # Database drivers for \#{label(group)}: sqlite3 on MRI/TruffleRuby,\n    # activerecord-jdbcsqlite3-adapter on JRuby (its release series tracks ActiveRecord).\n    \#{notes.join(\"\\n\")}\n    gem \#{sqlite3}\n\n    platforms :jruby do\n      gem \"activerecord-jdbcsqlite3-adapter\", \">= 1.3\"\n      gem \"bigdecimal\", \">= 1\"\n    end\n  GEMFILE\nend\n"

#eval_path_for(activerecord_version) ⇒ String

Returns the matching support gemfile, relative to gemfiles/ (for eval_gemfile).

Parameters:

  • activerecord_version (String, Gem::Version) —

    the entry's ActiveRecord version

Returns:

  • (String) —

    the matching support gemfile, relative to gemfiles/ (for eval_gemfile)



99
100
101
# File 'lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb', line 99

def eval_path_for(activerecord_version)
  path_for(group_for(activerecord_version)).delete_prefix("gemfiles/")
end

#generate(activerecord_versions) ⇒ Hash{String => Symbol}

Writes the support gemfile needed by each ActiveRecord version, skipping hand-maintained files, and removes support gemfiles generated by kettle-jem-appraisals 0.1.4 and earlier.

Parameters:

  • activerecord_versions (Array<String>) —

    ActiveRecord versions in the matrix

Returns:

  • (Hash{String => Symbol}) —

    relative path => :written, :kept, or :removed



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb', line 72

def generate(activerecord_versions)
  results = {}
  activerecord_versions.map { |version| group_for(version) }.uniq.each do |group|
    relative_path = path_for(group)
    absolute = File.join(base_dir, relative_path)
    if hand_maintained?(absolute)
      results[relative_path] = :kept
      next
    end

    FileUtils.mkdir_p(File.dirname(absolute))
    File.write(absolute, build_content(group))
    results[relative_path] = :written
  end

  LEGACY_PATHS.each do |relative_path|
    absolute = File.join(base_dir, relative_path)
    next unless File.exist?(absolute) && !hand_maintained?(absolute)

    FileUtils.rm_f(absolute)
    results[relative_path] = :removed
  end
  results
end

#sqlite3_requirements(activerecord_version) ⇒ Array<String>

Returns the sqlite3 requirements for that ActiveRecord version.

Parameters:

  • activerecord_version (String, Gem::Version)

Returns:

  • (Array<String>) —

    the sqlite3 requirements for that ActiveRecord version



105
106
107
108
# File 'lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb', line 105

def sqlite3_requirements(activerecord_version)
  group = group_for(activerecord_version)
  group ? group.first.requirements : []
end

#unfixed_sqlite3_advisories(activerecord_version) ⇒ Array<String>

Advisories that affect every sqlite3 series the requirements allow.

Parameters:

  • activerecord_version (String, Gem::Version)

Returns:

  • (Array<String>) —

    advisory ids; empty when a fixed sqlite3 release is allowed



114
115
116
117
118
119
120
121
122
# File 'lib/kettle/jem/appraisals/active_record_support_gemfile_generator.rb', line 114

def unfixed_sqlite3_advisories(activerecord_version)
  requirement = Gem::Requirement.new(*sqlite3_requirements(activerecord_version))
  allowed = Kettle::Rb::GemFloors.entries_for("sqlite3").select { |entry|
    requirement.satisfied_by?(Gem::Version.new(entry.floor))
  }
  return [] if allowed.empty? || allowed.any? { |entry| entry.unpatched_advisories.empty? }

  allowed.flat_map(&:unpatched_advisories).uniq.sort
end