with_model 2.1.7 → 2.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83aa1a692c521015759725b2641f53304ab13f6c3aac17550dc7b802c8e015ea
4
- data.tar.gz: e8c44ffe440d68a3f8311f2250a2184e88fffbe1d9713f0b870a5c15fcf8ec2e
3
+ metadata.gz: 809e5984c60e2fa27b71d3d3ce7664feae0a3b2c7a59f345e2a94a819affb623
4
+ data.tar.gz: 5e8b519ef7d338aa754755efd065af68b718e6dc1f96fe27632266752e7b4990
5
5
  SHA512:
6
- metadata.gz: 3acf78cc398a2c3c7612a1ee534d3d11ed93f701b8cbfaa218d8b64fb5c3dc0a3a5cf23080f3853e97cf1ba72a9cc202673f058a7c784213287bf9b81e0a1e85
7
- data.tar.gz: 0b8ccf8fa27e19880fbfdd166d3eb0974cecaecc94955c2d32204306eb3ca48f050f2b578a9042d81a34e0bfc7d6ab14e5c42b16d802e4b16cfc7efeb38bfecc
6
+ metadata.gz: cf09e4aabe724638f054a54199d62a61ae9efd0cca28485412f85fd082a99f104d4f538bd7041f1777c9380398e7d1497d5f01dcc07cb5fdacc116ad3e7dd69f
7
+ data.tar.gz: 6f795b756e9b33abd148d51010a0afb80e01660af35c76d1a98a634c6b2d31c38a616e94aeff99c6d8b19b019c69b994c9b5dc21e69439fbbcb004528dc3606b
@@ -6,8 +6,6 @@ on:
6
6
  - master
7
7
  - github-actions
8
8
  pull_request:
9
- branches:
10
- - master
11
9
 
12
10
  jobs:
13
11
  test:
@@ -17,18 +15,31 @@ jobs:
17
15
  strategy:
18
16
  fail-fast: false
19
17
  matrix:
20
- ruby-version: ['2.7', '3.0', '3.1', '3.2']
18
+ ruby-version: ['3.1', '3.2', '3.3', '3.4']
21
19
  active-record-version-env:
22
- - ACTIVE_RECORD_VERSION="~> 6.0.0"
23
- - ACTIVE_RECORD_VERSION="~> 6.1.0"
24
20
  - ACTIVE_RECORD_VERSION="~> 7.0.0"
21
+ - ACTIVE_RECORD_VERSION="~> 7.1.0"
22
+ - ACTIVE_RECORD_VERSION="~> 7.2.0"
23
+ - ACTIVE_RECORD_VERSION="~> 8.0.0"
25
24
  allow-failure: [false]
25
+ exclude:
26
+ - ruby-version: '3.1'
27
+ active-record-version-env: ACTIVE_RECORD_VERSION="~> 8.0.0"
26
28
  include:
27
- - ruby-version: '3.2'
28
- active-record-version-env: ACTIVE_RECORD_BRANCH="7-0-stable"
29
+ - ruby-version: '3.4'
30
+ active-record-version-env: ACTIVE_RECORD_BRANCH="main"
31
+ allow-failure: true
32
+ - ruby-version: '3.4'
33
+ active-record-version-env: ACTIVE_RECORD_BRANCH="8-0-stable"
29
34
  allow-failure: true
30
- - ruby-version: '3.2'
31
- active-record-version-env: ACTIVE_RECORD_BRANCH="6-1-stable"
35
+ - ruby-version: '3.4'
36
+ active-record-version-env: ACTIVE_RECORD_BRANCH="7-2-stable"
37
+ allow-failure: true
38
+ - ruby-version: '3.4'
39
+ active-record-version-env: ACTIVE_RECORD_BRANCH="7-1-stable"
40
+ allow-failure: true
41
+ - ruby-version: '3.4'
42
+ active-record-version-env: ACTIVE_RECORD_BRANCH="7-0-stable"
32
43
  allow-failure: true
33
44
  continue-on-error: ${{ matrix.allow-failure }}
34
45
  steps:
data/.rspec CHANGED
@@ -1,3 +1,3 @@
1
1
  --color
2
2
  --format documentation
3
- --order default
3
+ --order defined
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ### 2.2.0
2
+
3
+ - Fix dependency tracking issue when `cache_classes: true` is set in Rails 7+.
4
+ - Create table in same database as `superclass` (Joe Lind)
5
+ - Switch to standard instead of rubocop for linting.
6
+ - Add support for Active Record 7 and 8.
7
+ - Require Active Record 7 or later.
8
+ - Add support for Ruby 3.3.
9
+ - Require Ruby 3.1 or later.
10
+
1
11
  ### 2.1.7
2
12
  - Require Ruby 2.7 or later
3
13
  - Add Ruby 3.2 support
data/Gemfile CHANGED
@@ -1,33 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source 'https://rubygems.org'
3
+ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
- ar_branch = ENV.fetch('ACTIVE_RECORD_BRANCH', nil)
8
- ar_version = ENV.fetch('ACTIVE_RECORD_VERSION', nil)
9
- is_jruby = RUBY_PLATFORM == 'java'
7
+ ar_branch = ENV.fetch("ACTIVE_RECORD_BRANCH", nil)
8
+ ar_version = ENV.fetch("ACTIVE_RECORD_VERSION", nil)
10
9
 
11
10
  if ar_branch
12
- gem 'activerecord', git: 'https://github.com/rails/rails.git', branch: ar_branch
13
- if ar_branch == 'master'
14
- gem 'arel', git: 'https://github.com/rails/arel.git'
15
- gem 'activerecord-jdbcsqlite3-adapter', git: 'https://github.com/jruby/activerecord-jdbc-adapter.git' if is_jruby
16
- end
11
+ gem "activerecord", git: "https://github.com/rails/rails.git", branch: ar_branch
12
+ gem "arel", git: "https://github.com/rails/arel.git" if ar_branch == "master"
17
13
  elsif ar_version
18
- gem 'activerecord', ar_version
19
- if is_jruby && !Gem::Requirement.new(ar_version).satisfied_by?(Gem::Version.new('5.2.0'))
20
- gem 'activerecord-jdbcsqlite3-adapter', git: 'https://github.com/jruby/activerecord-jdbc-adapter.git'
21
- end
14
+ gem "activerecord", ar_version
22
15
  end
23
16
 
24
- gem 'bundler'
25
- gem 'minitest'
26
- gem 'rake'
27
- gem 'rspec'
28
- gem 'rubocop'
29
- gem 'rubocop-minitest'
30
- gem 'rubocop-rake'
31
- gem 'rubocop-rspec'
32
- gem 'simplecov'
33
- gem 'sqlite3', '~> 1.6.0' unless is_jruby
17
+ gem "bigdecimal"
18
+ gem "bundler"
19
+ gem "debug"
20
+ gem "logger"
21
+ gem "minitest"
22
+ gem "mutex_m"
23
+ gem "rake"
24
+ gem "rspec"
25
+ gem "standard"
26
+
27
+ if ar_branch == "7-0-stable" || ar_version == "~> 7.0.0"
28
+ gem "sqlite3", "< 2"
29
+ else
30
+ gem "sqlite3"
31
+ end
data/Rakefile CHANGED
@@ -1,23 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
- require 'rake/testtask'
6
- require 'rubocop/rake_task'
3
+ require "bundler/gem_tasks"
7
4
 
8
- desc 'Run specs'
9
- RSpec::Core::RakeTask.new(:spec) do |t|
10
- t.pattern = Dir.glob('spec/**/*_spec.rb')
11
- end
5
+ require "rspec/core/rake_task"
6
+ RSpec::Core::RakeTask.new
12
7
 
13
- desc 'Run tests'
14
- Rake::TestTask.new(:test) do |t|
15
- t.libs << 'test'
16
- t.libs << 'lib'
17
- t.pattern = 'test/**/*_test.rb'
18
- end
8
+ require "minitest/test_task"
9
+ Minitest::TestTask.create
19
10
 
20
- desc 'Run lint'
21
- RuboCop::RakeTask.new
11
+ # standard rake task
12
+ require "standard/rake"
22
13
 
23
- task default: %i[spec test rubocop]
14
+ task default: %i[spec test standard]
@@ -37,7 +37,7 @@ module WithModel
37
37
  end
38
38
 
39
39
  def lookup_list
40
- @const_name.to_s.split('::')
40
+ @const_name.to_s.split("::")
41
41
  end
42
42
 
43
43
  def basename
@@ -0,0 +1,87 @@
1
+ require "active_support/descendants_tracker"
2
+
3
+ module WithModel
4
+ # Based on https://github.com/rails/rails/blob/491afff27e2dd3d5f301b478b9a43d3c31709af8/activesupport/lib/active_support/descendants_tracker.rb
5
+ module DescendantsTracker
6
+ if RUBY_ENGINE == "ruby"
7
+ # On MRI `ObjectSpace::WeakMap` keys are weak references.
8
+ # So we can simply use WeakMap as a `Set`.
9
+ class WeakSet < ObjectSpace::WeakMap # :nodoc:
10
+ alias_method :to_a, :keys
11
+
12
+ def <<(object)
13
+ self[object] = true
14
+ end
15
+ end
16
+ else
17
+ # On TruffleRuby `ObjectSpace::WeakMap` keys are strong references.
18
+ # So we use `object_id` as a key and the actual object as a value.
19
+ #
20
+ # JRuby for now doesn't have Class#descendant, but when it will, it will likely
21
+ # have the same WeakMap semantic than Truffle so we future proof this as much as possible.
22
+ class WeakSet # :nodoc:
23
+ def initialize
24
+ @map = ObjectSpace::WeakMap.new
25
+ end
26
+
27
+ def [](object)
28
+ @map.key?(object.object_id)
29
+ end
30
+ alias_method :include?, :[]
31
+
32
+ def []=(object, _present)
33
+ @map[object.object_id] = object
34
+ end
35
+
36
+ def to_a
37
+ @map.values
38
+ end
39
+
40
+ def <<(object)
41
+ self[object] = true
42
+ end
43
+ end
44
+ end
45
+ @excluded_descendants = WeakSet.new
46
+
47
+ class << self
48
+ def clear(classes) # :nodoc:
49
+ classes.each do |klass|
50
+ @excluded_descendants << klass
51
+ klass.descendants.each do |descendant|
52
+ @excluded_descendants << descendant
53
+ end
54
+ end
55
+ end
56
+
57
+ def reject!(classes) # :nodoc:
58
+ if @excluded_descendants
59
+ classes.reject! { |d| @excluded_descendants.include?(d) }
60
+ end
61
+ classes
62
+ end
63
+ end
64
+
65
+ module DestroyedClassesFiltering
66
+ def subclasses
67
+ WithModel::DescendantsTracker.reject!(super)
68
+ end
69
+
70
+ def descendants
71
+ WithModel::DescendantsTracker.reject!(super)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ class ActiveRecord::Base
78
+ extend WithModel::DescendantsTracker::DestroyedClassesFiltering
79
+ end
80
+
81
+ module ActiveSupport
82
+ module DescendantsTracker
83
+ class << self
84
+ attr_reader :clear_disabled
85
+ end
86
+ end
87
+ end
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record'
4
- require 'active_support/core_ext/string/inflections'
5
- require 'English'
6
- require 'with_model/constant_stubber'
7
- require 'with_model/methods'
8
- require 'with_model/table'
3
+ require "logger"
4
+ require "active_record"
5
+ require "active_support/core_ext/string/inflections"
6
+ require "English"
7
+ require "with_model/constant_stubber"
8
+ require "with_model/descendants_tracker"
9
+ require "with_model/methods"
10
+ require "with_model/table"
9
11
 
10
12
  module WithModel
11
13
  # In general, direct use of this class should be avoided. Instead use
@@ -38,6 +40,7 @@ module WithModel
38
40
  cleanup_descendants_tracking
39
41
  reset_dependencies_cache
40
42
  table.destroy
43
+ WithModel::DescendantsTracker.clear([@model])
41
44
  @model = nil
42
45
  end
43
46
 
@@ -54,15 +57,8 @@ module WithModel
54
57
  end
55
58
 
56
59
  def cleanup_descendants_tracking
57
- if defined?(ActiveSupport::DescendantsTracker)
58
- if ActiveSupport::VERSION::MAJOR >= 7
59
- ActiveSupport::DescendantsTracker.clear([@model])
60
- else
61
- ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).delete(ActiveRecord::Base)
62
- end
63
- elsif @model.superclass.respond_to?(:direct_descendants)
64
- @model.superclass.direct_descendants.delete(@model)
65
- end
60
+ ActiveSupport::DescendantsTracker.clear([@model]) \
61
+ unless ActiveSupport::DescendantsTracker.clear_disabled
66
62
  end
67
63
 
68
64
  def reset_dependencies_cache
@@ -76,7 +72,7 @@ module WithModel
76
72
  end
77
73
 
78
74
  def table
79
- @table ||= Table.new table_name, @table_options, &@table_block
75
+ @table ||= Table.new table_name, @table_options, connection: @superclass.connection, &@table_block
80
76
  end
81
77
 
82
78
  def table_name
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record'
3
+ require "active_record"
4
4
 
5
5
  module WithModel
6
6
  # In general, direct use of this class should be avoided. Instead use
@@ -8,12 +8,14 @@ module WithModel
8
8
  class Table
9
9
  # @param [Symbol] name The name of the table to create.
10
10
  # @param options Passed to ActiveRecord `create_table`.
11
+ # @param connection The connection to use for creating the table.
11
12
  # @param block Passed to ActiveRecord `create_table`.
12
13
  # @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-create_table
13
- def initialize(name, options = {}, &block)
14
+ def initialize(name, options = {}, connection: ActiveRecord::Base.connection, &block)
14
15
  @name = name.freeze
15
16
  @options = options.freeze
16
17
  @block = block
18
+ @connection = connection
17
19
  end
18
20
 
19
21
  # Creates the table with the initialized options. Drops the table if
@@ -29,6 +31,8 @@ module WithModel
29
31
 
30
32
  private
31
33
 
34
+ attr_reader :connection
35
+
32
36
  def exists?
33
37
  if connection.respond_to?(:data_source_exists?)
34
38
  connection.data_source_exists?(@name)
@@ -36,9 +40,5 @@ module WithModel
36
40
  connection.table_exists?(@name)
37
41
  end
38
42
  end
39
-
40
- def connection
41
- ActiveRecord::Base.connection
42
- end
43
43
  end
44
44
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module WithModel
4
- VERSION = '2.1.7'
4
+ VERSION = "2.2.0"
5
5
  end
data/lib/with_model.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'with_model/model'
4
- require 'with_model/model/dsl'
5
- require 'with_model/table'
6
- require 'with_model/version'
3
+ require "with_model/model"
4
+ require "with_model/model/dsl"
5
+ require "with_model/table"
6
+ require "with_model/version"
7
7
 
8
8
  module WithModel
9
9
  class MiniTestLifeCycle < Module
@@ -61,7 +61,7 @@ module WithModel
61
61
  # @param [Object] object The new model object instance to create
62
62
  # @param scope Passed to `before`/`after` in the test context. Rspec only.
63
63
  # @param [Symbol] runner The test running, either :rspec or :minitest, defaults to :rspec
64
- def setup_object(object, scope: nil, runner: nil) # rubocop:disable Metrics/MethodLength
64
+ def setup_object(object, scope: nil, runner: nil)
65
65
  case runner || WithModel.runner
66
66
  when :rspec
67
67
  before(*scope) do
@@ -76,7 +76,7 @@ module WithModel
76
76
  include MiniTestLifeCycle.call(object)
77
77
  end
78
78
  else
79
- raise ArgumentError, 'Unsupported test runner set, expected :rspec or :minitest'
79
+ raise ArgumentError, "Unsupported test runner set, expected :rspec or :minitest"
80
80
  end
81
81
  end
82
82
  end
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
3
+ require "spec_helper"
4
4
 
5
- describe 'ActiveRecord behaviors' do
6
- describe 'a temporary ActiveRecord model created with with_model' do
7
- context 'that has a named scope' do
5
+ describe "ActiveRecord behaviors" do
6
+ describe "a temporary ActiveRecord model created with with_model" do
7
+ context "that has a named scope" do
8
8
  before do
9
9
  @regular_model = Class.new ActiveRecord::Base do
10
- self.table_name = 'regular_models'
11
- scope :title_is_foo, -> { where(title: 'foo') }
10
+ self.table_name = "regular_models"
11
+ scope :title_is_foo, -> { where(title: "foo") }
12
12
  end
13
13
 
14
14
  @regular_model.connection.create_table(@regular_model.table_name, force: true) do |t|
15
- t.string 'title'
16
- t.text 'content'
15
+ t.string "title"
16
+ t.text "content"
17
17
  t.timestamps null: false
18
18
  end
19
19
  end
@@ -24,37 +24,37 @@ describe 'ActiveRecord behaviors' do
24
24
 
25
25
  with_model :BlogPost do
26
26
  table do |t|
27
- t.string 'title'
28
- t.text 'content'
27
+ t.string "title"
28
+ t.text "content"
29
29
  t.timestamps null: false
30
30
  end
31
31
 
32
32
  model do
33
- scope :title_is_foo, -> { where(title: 'foo') }
33
+ scope :title_is_foo, -> { where(title: "foo") }
34
34
  end
35
35
  end
36
36
 
37
- describe 'the named scope' do
38
- it 'works like a regular named scope' do
39
- included = @regular_model.create!(title: 'foo', content: 'Include me!')
40
- @regular_model.create!(title: 'bar', content: 'Include me!')
37
+ describe "the named scope" do
38
+ it "works like a regular named scope" do
39
+ included = @regular_model.create!(title: "foo", content: "Include me!")
40
+ @regular_model.create!(title: "bar", content: "Include me!")
41
41
 
42
42
  expect(@regular_model.title_is_foo).to eq [included]
43
43
 
44
- included = BlogPost.create!(title: 'foo', content: 'Include me!')
45
- BlogPost.create!(title: 'bar', content: 'Include me!')
44
+ included = BlogPost.create!(title: "foo", content: "Include me!")
45
+ BlogPost.create!(title: "bar", content: "Include me!")
46
46
 
47
47
  expect(BlogPost.title_is_foo).to eq [included]
48
48
  end
49
49
  end
50
50
  end
51
51
 
52
- context 'that has a polymorphic belongs_to' do
52
+ context "that has a polymorphic belongs_to" do
53
53
  before do
54
54
  animal = Class.new ActiveRecord::Base do
55
55
  has_many :tea_cups, as: :pet
56
56
  end
57
- stub_const 'Animal', animal
57
+ stub_const "Animal", animal
58
58
  end
59
59
 
60
60
  with_model :TeaCup do
@@ -74,24 +74,24 @@ describe 'ActiveRecord behaviors' do
74
74
  end
75
75
  end
76
76
 
77
- describe 'the polymorphic belongs_to' do
78
- it 'works like a regular polymorphic belongs_to' do
77
+ describe "the polymorphic belongs_to" do
78
+ it "works like a regular polymorphic belongs_to" do
79
79
  animal = Animal.create!
80
80
  stuffed_animal = StuffedAnimal.create!
81
81
 
82
82
  tea_cup_for_animal = TeaCup.create!(pet: animal)
83
- expect(tea_cup_for_animal.pet_type).to eq 'Animal'
83
+ expect(tea_cup_for_animal.pet_type).to eq "Animal"
84
84
  expect(animal.tea_cups).to include(tea_cup_for_animal)
85
85
 
86
86
  tea_cup_for_stuffed_animal = TeaCup.create!(pet: stuffed_animal)
87
- expect(tea_cup_for_stuffed_animal.pet_type).to eq 'StuffedAnimal'
87
+ expect(tea_cup_for_stuffed_animal.pet_type).to eq "StuffedAnimal"
88
88
  expect(stuffed_animal.tea_cups).to include(tea_cup_for_stuffed_animal)
89
89
  end
90
90
  end
91
91
  end
92
92
  end
93
93
 
94
- context 'with an association' do
94
+ context "with an association" do
95
95
  with_model :Province do
96
96
  table do |t|
97
97
  t.belongs_to :country
@@ -103,13 +103,13 @@ describe 'ActiveRecord behaviors' do
103
103
 
104
104
  with_model :Country
105
105
 
106
- context 'in earlier examples' do
107
- it 'works as normal' do
106
+ context "in earlier examples" do
107
+ it "works as normal" do
108
108
  expect { Province.create!(country: Country.create!) }.not_to raise_error
109
109
  end
110
110
  end
111
111
 
112
- context 'in later examples' do
112
+ context "in later examples" do
113
113
  it "does not hold a reference to earlier example groups' classes" do
114
114
  expect(Province.reflect_on_association(:country).klass).to eq Country
115
115
  end
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
3
+ require "spec_helper"
4
4
 
5
5
  module WithModel
6
6
  describe ConstantStubber do
7
- it 'allows calling unstub_const multiple times' do
8
- stubber = described_class.new('Foo')
7
+ it "allows calling unstub_const multiple times" do
8
+ stubber = described_class.new("Foo")
9
9
  stubber.stub_const(1)
10
10
  expect { 2.times { stubber.unstub_const } }.not_to raise_error
11
11
  end
12
12
 
13
- it 'allows calling unstub_const without stub_const' do
14
- stubber = described_class.new('Foo')
13
+ it "allows calling unstub_const without stub_const" do
14
+ stubber = described_class.new("Foo")
15
15
  expect { stubber.unstub_const }.not_to raise_error
16
16
  end
17
17
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe "Descendants tracking" do
6
+ with_model :BlogPost do
7
+ model do
8
+ def self.inspect
9
+ "BlogPost class #{object_id}"
10
+ end
11
+ end
12
+ end
13
+
14
+ def blog_post_classes
15
+ ActiveRecord::Base.descendants.select do |c|
16
+ c.table_name == BlogPost.table_name
17
+ end
18
+ end
19
+
20
+ shared_examples "clearing descendants between test runs" do
21
+ it "includes the correct model class in descendants on the first test run" do
22
+ expect(blog_post_classes).to eq [BlogPost]
23
+ end
24
+
25
+ it "includes the correct model class in descendants on the second test run" do
26
+ expect(blog_post_classes).to eq [BlogPost]
27
+ end
28
+ end
29
+
30
+ context "with ActiveSupport::DescendantsTracker (cache_classes: true)" do
31
+ before do
32
+ expect(ActiveSupport::DescendantsTracker.clear_disabled).to be_falsey
33
+ expect { ActiveSupport::DescendantsTracker.clear([]) }.not_to raise_exception
34
+ end
35
+
36
+ include_examples "clearing descendants between test runs"
37
+ end
38
+
39
+ context "without ActiveSupport::DescendantsTracker (cache_classes: false)" do
40
+ before do
41
+ ActiveSupport::DescendantsTracker.disable_clear!
42
+ expect(ActiveSupport::DescendantsTracker.clear_disabled).to be_truthy
43
+ expect { ActiveSupport::DescendantsTracker.clear([]) }.to raise_exception(RuntimeError)
44
+ end
45
+
46
+ include_examples "clearing descendants between test runs"
47
+ end
48
+ end