From 39cdc27af5649fd140fccda56a701d87ef85730a Mon Sep 17 00:00:00 2001 From: bivanalhar Date: Fri, 31 May 2024 18:08:39 +0800 Subject: [PATCH] chore(version): bump into 4.0.0 - specify exact Rails 6.0 for testing - add rails 7.1 for testing - use yml instead of travis for testing - test supermodel methods being modifiable - test adjustable scoping - test exists? method being correctly defined - add find_or_initialize_by_spec test - include models in migration spec - minor modification in models and spec_helper - added Eraser model for testing - add many entities of Eraser inside the test when doing actables test - safeguard scope to be options in actable --- .github/workflows/ruby.yml | 41 +++++++++++++ .travis.yml | 14 ----- Appraisals | 6 +- CHANGELOG.md | 85 +++++++++++++++++++++++++++ README.md | 12 ++-- active_record-acts_as.gemspec | 2 +- gemfiles/rails_6.0.gemfile | 2 +- gemfiles/rails_7.1.gemfile | 8 +++ lib/active_record/acts_as/relation.rb | 4 +- lib/active_record/acts_as/version.rb | 2 +- spec/acts_as_spec.rb | 85 ++++++++++++++++++++------- spec/find_or_initialize_by_spec.rb | 23 ++++++++ spec/migrations_spec.rb | 1 + spec/models.rb | 26 ++++++++ spec/spec_helper.rb | 6 ++ 15 files changed, 271 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/ruby.yml delete mode 100644 .travis.yml create mode 100644 gemfiles/rails_7.1.gemfile create mode 100644 spec/find_or_initialize_by_spec.rb diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml new file mode 100644 index 0000000..4d90c0a --- /dev/null +++ b/.github/workflows/ruby.yml @@ -0,0 +1,41 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake +# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby + +name: Ruby + +on: + push: + branches: ['master'] + pull_request: + branches: ['master'] + schedule: + - cron: '0 0 * * 0' + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['3.0', '3.1', '3.2'] + activerecord: ['6.0', '6.1', '7.0', '7.1'] + env: + BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails_${{ matrix.activerecord }}.gemfile + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, + # change this to (see https://github.com/ruby/setup-ruby#versioning): + # uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run tests + run: bundle exec rake diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3a985bf..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: ruby -sudo: false -rvm: - - 2.2.7 - - 2.3.4 - - 2.4.1 -before_install: - - gem update --system - - gem update bundler -gemfile: - - gemfiles/rails_4.2.gemfile - - gemfiles/rails_5.0.gemfile - - gemfiles/rails_5.1.gemfile - - gemfiles/rails_5.2.gemfile diff --git a/Appraisals b/Appraisals index e150b90..a7c1e05 100644 --- a/Appraisals +++ b/Appraisals @@ -1,5 +1,5 @@ appraise "rails-6.0" do - gem 'rails', '~> 6.0' + gem 'rails', '6.0.6.1' end appraise "rails-6.1" do @@ -9,3 +9,7 @@ end appraise "rails-7.0" do gem 'rails', '~> 7.0' end + +appraise "rails-7.1" do + gem 'rails', '~> 7.1' +end diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ddf32d..1f5d6e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,95 +1,180 @@ # Change Log + All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [4.0.0] - 2024-06-03 + +### Added + +- Add support for rails 6, 6.1, 7, 7.1 while removing rails 4.x and 5.x + from the travis matrix. +- Add support for ruby 3.0, 3.1, and 3.2 +- Update test coverage +- Add `.exists?` support to seamlessly check in both the model and super model + +### Fixed + +- Bump minimum ruby version to 3.0 +- Collection methods such as `<<` work now under rails 6.1 +- Prepare for Rails 6.2 breaking change by updating how errors + are accessed and removing warning. They are now ruby objects. + see [this](https://api.rubyonrails.org/v6.1.0/classes/ActiveModel/Errors.html) +- Fixed rails `6.0.x` compatibility for `ActiveRecord::Errors` +- Handle kwargs in ruby 3 in methods delegated to supermodel +- Fix `.actables` to restrict based on type. + +### Removed + +- Remove last ruby 3 warnings and make the gem **totally compatible with + ruby 3** +- Remove support for rails 4.x and 5.x, as well as ruby 2.x + +## [3.0.1] - 2018-04-25 + +### Changed + +- Remove bi-directional autosave and use callbacks +- Ensure that non_cyclic_save does not get called for a new_record + +## [3.0.0] - 2019-02-21 + +## [2.5.0] - 2017-07-29 + +### Changed + +- Drop support for Rails >= 5.0 +- Remove warnings occurring in Rails 5.1 + ## [2.4.2] - 2017-04-20 + ### Fixed + - Fix querying for conditions with hashes. ## [2.4.1] - 2017-04-19 + ### Fixed + - Make ActiveRecord::Relation#where! work. ## [2.4.0] - 2017-04-16 + ### Changed + - Don't make all supermodel class methods callable by submodel, only scopes. Add `callable_by_submodel` to supermodel so users can make their own class methods callable by submodels. ## [2.3.1] - 2017-04-15 + ### Fixed + - Make calling supermodel class methods work through relations/associations as well ## [2.3.0] - 2017-04-12 + ### Fixed + - Prevent duplicate validation errors (fixes https://github.com/krautcomputing/active_record-acts_as/issues/2) ### Added + - Added support for touching submodel attributes (https://github.com/krautcomputing/active_record-acts_as/pull/3, thanks to [dezmathio](https://github.com/dezmathio)!) ## [2.2.1] - 2017-04-08 + ### Fixed + - Make sure submodel instance changes are retained when calling `submodel_instance.acting_as.specific` ## [2.2.0] - 2017-04-08 + ### Added + - Added support for calling superclass methods on the subclass or subclass relations ## [2.1.1] - 2017-03-22 + ### Fixed + - Fix querying subclass with `where`, for `enum` (and possibly other) attributes the detection whether the attribute is defined on the superclass or subclass didn't work. ## [2.1.0] - 2017-03-17 + ### Added + - Access superobjects from query on submodel by calling `.actables` ## [2.0.9] - 2017-03-02 + ### Fixed + - Fix handling of query conditions that contain a dot ## [2.0.8] - 2017-02-17 + ### Fixed + - Avoid circular dependency on destroy ## [2.0.7] - 2017-02-17 [YANKED] + ### Fixed + - Set reference to submodel when building supermodel ## [2.0.6] - 2017-02-17 + ### Added + - Allow arguments to #touch and forward them to the supermodel ## [2.0.5] - 2016-12-20 + ### Fixed + - Don't try to touch supermodel if it's not persisted - Call `#destroy`, not `#delete`, on the submodule by default to trigger callbacks ## [2.0.4] - 2016-12-07 + ### Fixed + - Touch associated objects if supermodel is updated ## [2.0.3] - 2016-11-07 + ### Fixed + - Fix defining associations on `acting_as` model after calling `acting_as` ## [2.0.2] - 2016-11-06 + ### Fixed + - Call `#touch` on `actable` object when it's called on the `acting_as` object ## [2.0.1] - 2016-10-05 + ### Added + - Added this changelog - Added `touch` option to skip touching the `acting_as` object (https://github.com/hzamani/active_record-acts_as/pull/78, thanks to [allenwq](https://github.com/allenwq)!) ## [2.0.0] - 2016-09-14 + ### Added + - Added support for Rails 5 (https://github.com/hzamani/active_record-acts_as/pull/80, thanks to [nicklandgrebe](https://github.com/nicklandgrebe)!) - Allow specifying `association_method` parameter (https://github.com/hzamani/active_record-acts_as/pull/72, thanks to [tombowo](https://github.com/tombowo)!) ### Removed + - Dropped support for Ruby < 2.2 and ActiveSupport/ActiveRecord < 4.2 ### Fixed + - Fixed `remove_actable` migration helper (https://github.com/hzamani/active_record-acts_as/pull/71, thanks to [nuclearpidgeon](https://github.com/nuclearpidgeon)!) [Unreleased]: https://github.com/krautcomputing/active_record-acts_as/compare/v2.4.2...HEAD diff --git a/README.md b/README.md index e171660..0a879de 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ [![Coverage Status](https://coveralls.io/repos/krautcomputing/active_record-acts_as/badge.png)](https://coveralls.io/r/krautcomputing/active_record-acts_as) [![Dependency Status](https://gemnasium.com/krautcomputing/active_record-acts_as.svg)](https://gemnasium.com/krautcomputing/active_record-acts_as) +# Change in the Upstream Repo + +This branch, initially forked from [the fork of `hzamani`](https://github.com/hzamani/active_record-acts_as), has now been changed into [the fork of `manuelmeurer`](https://github.com/chaadow/active_record-acts_as) since the former repo has mentioned that the currently active development is taking place in the latter one. The latest update for this repo (v4.0.0) follows the version 5.2.0 of `manuelmeurer`'s repo. + # ActiveRecord::ActsAs This is a refactor of [`acts_as_relation`](https://github.com/hzamani/acts_as_relation) @@ -21,9 +25,9 @@ a separate table for each product type, i.e. a `pens` table with `color` column. ## Requirements -* Ruby >= 2.2 -* ActiveSupport >= 4.2 -* ActiveRecord >= 4.2 +- Ruby >= 3.0, <= 3.2 +- ActiveSupport >= 6.0, <= 7.1 +- ActiveRecord >= 6.0, <= 7.1 ## Installation @@ -189,13 +193,11 @@ end Multiple `acts_as` in the same class are not supported! - ## Migrating from acts_as_relation Replace `acts_as_superclass` in models with `actable` and if you where using `:as_relation_superclass` option on `create_table` remove it and use `t.actable` on column definitions. - ## RSpec custom matchers To use this library custom RSpec matchers, you must require the `rspec/acts_as_matchers` file. diff --git a/active_record-acts_as.gemspec b/active_record-acts_as.gemspec index a8b19ca..ff6b459 100644 --- a/active_record-acts_as.gemspec +++ b/active_record-acts_as.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 3.0" - spec.add_development_dependency "sqlite3" + spec.add_development_dependency "sqlite3", "~> 1.7" spec.add_development_dependency "bundler" spec.add_development_dependency "rspec", "~> 3" spec.add_development_dependency "psych", "3.3.2" diff --git a/gemfiles/rails_6.0.gemfile b/gemfiles/rails_6.0.gemfile index ea48f46..3135523 100644 --- a/gemfiles/rails_6.0.gemfile +++ b/gemfiles/rails_6.0.gemfile @@ -3,6 +3,6 @@ source "https://rubygems.org" gem "coveralls", require: false -gem "rails", "~> 6.0" +gem "rails", "6.0.6.1" gemspec path: "../" diff --git a/gemfiles/rails_7.1.gemfile b/gemfiles/rails_7.1.gemfile new file mode 100644 index 0000000..d6b738d --- /dev/null +++ b/gemfiles/rails_7.1.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "coveralls", require: false +gem "rails", "~> 7.1" + +gemspec path: "../" diff --git a/lib/active_record/acts_as/relation.rb b/lib/active_record/acts_as/relation.rb index 3a0eaa5..6589854 100644 --- a/lib/active_record/acts_as/relation.rb +++ b/lib/active_record/acts_as/relation.rb @@ -4,7 +4,7 @@ module Relation extend ActiveSupport::Concern module ClassMethods - def acts_as(name, scope = nil, options = {}) + def acts_as(name, scope = nil, **options) options, scope = scope, nil if Hash === scope association_method = options.delete(:association_method) @@ -84,6 +84,8 @@ def is_a?(klass) end def actable(scope = nil, **options) + options, scope = scope, nil if Hash === scope + name = options.delete(:as) || :actable reflections = belongs_to(name, scope, **options.reverse_merge(validate: false, diff --git a/lib/active_record/acts_as/version.rb b/lib/active_record/acts_as/version.rb index 56520d0..a83adf2 100644 --- a/lib/active_record/acts_as/version.rb +++ b/lib/active_record/acts_as/version.rb @@ -1,5 +1,5 @@ module ActiveRecord module ActsAs - VERSION = "3.0.1" + VERSION = "4.0.0" end end diff --git a/spec/acts_as_spec.rb b/spec/acts_as_spec.rb index 2224d0a..a47fe6c 100644 --- a/spec/acts_as_spec.rb +++ b/spec/acts_as_spec.rb @@ -4,7 +4,9 @@ subject { Pen } let(:pen_attributes) { {name: 'pen', price: 0.8, color: 'red'} } + let(:eraser_attributes) { {name: 'eraser', price: 1.2, strength: 3} } let(:pen) { Pen.new pen_attributes } + let(:eraser) { Eraser.new eraser_attributes } let(:isolated_pen) { IsolatedPen.new color: 'red' } let(:store) { Store.new name: 'biggerman' } let(:product) { Product.new store: store } @@ -157,6 +159,10 @@ expect(pen.present).to eq("pen - $0.8") end + it "responds to supermodel methods with keyword arguments" do + expect(pen.keyword_method(one: 3, two: 4)).to eq [3,4] + end + it 'responds to serialized attribute' do expect(pen).to respond_to('option1') expect(isolated_pen).to respond_to('option2') @@ -295,26 +301,26 @@ end context "errors" do + let(:error_message) { "can't be blank" } + context 'when validates_actable is set to true' do it "combines supermodel and submodel errors" do pen = Pen.new expect(pen).to be_invalid - expect(pen.errors.to_h).to eq( - name: "can't be blank", - price: "can't be blank", - color: "can't be blank" + expect(pen.errors.to_hash).to eq( + name: [error_message], + price: [error_message], + color: [error_message] ) pen.name = 'testing' expect(pen).to be_invalid - expect(pen.errors.to_h).to eq( - price: "can't be blank", - color: "can't be blank" + expect(pen.errors.to_hash).to eq( + price: [error_message], + color: [error_message] ) pen.color = 'red' expect(pen).to be_invalid - expect(pen.errors.to_h).to eq( - price: "can't be blank" - ) + expect(pen.errors.to_hash).to eq( { price: [error_message] }) pen.price = 0.8 expect(pen).to be_valid end @@ -324,9 +330,7 @@ it "unless validates_actable is set to false" do pen = IsolatedPen.new expect(pen).to be_invalid - expect(pen.errors.to_h).to eq( - color: "can't be blank" - ) + expect(pen.errors.to_hash).to eq( { color: [error_message] }) pen.color = 'red' expect(pen).to be_valid end @@ -397,12 +401,16 @@ end describe ".actables" do - before(:each) { clear_database } + before { clear_database } it "returns a query for the actable records" do red_pen = Pen.create!(name: 'red pen', price: 0.8, color: 'red') blue_pen = Pen.create!(name: 'blue pen', price: 0.8, color: 'blue') - black_pen = Pen.create!(name: 'black pen', price: 0.9, color: 'black') + _black_pen = Pen.create!(name: 'black pen', price: 0.9, color: 'black') + + 20.times do + Eraser.create!(name: 'eraser', price: 1.2, strength: 3) + end actables = Pen.where(price: 0.8).actables @@ -411,12 +419,34 @@ end end + describe '.actable' do + class User < ActiveRecord::Base + actable -> { unscope(:where) } + end + + class Customer < ActiveRecord::Base + default_scope { where('identifier > 1') } + acts_as :user + + validates_presence_of :identifier + end + + context 'with scope' do + it 'unscopes default scope' do + customer = Customer.create!(identifier: 1) + user = customer.user.reload + expect(user).to be_a User + expect(user.actable).to eq(customer) + end + end + end + context 'class methods' do before(:each) { clear_database } context 'when they are defined via `scope`' do it 'can be called from the submodel' do - cheap_pen = Pen.create!(name: 'cheap pen', price: 0.5, color: 'blue') + _cheap_pen = Pen.create!(name: 'cheap pen', price: 0.5, color: 'blue') expensive_pen = Pen.create!(name: 'expensive pen', price: 1, color: 'red') expect(Product.with_price_higher_than(0.5).to_a).to eq([expensive_pen.acting_as]) @@ -429,6 +459,10 @@ expect(Product.class_method_callable_by_submodel).to eq('class_method_callable_by_submodel') expect(Pen.class_method_callable_by_submodel).to eq('class_method_callable_by_submodel') end + + it 'with keyword arguments can be called from the submodel' do + expect(Pen.class_keyword_method_callable_by_submodel(one: 3, two: 4)).to eq([3,4]) + end end context 'when they are neither defined via `scope` nor made callable by submodel' do @@ -456,6 +490,13 @@ @black_pen.buyers.create! name: 'John' end + describe 'exists?' do + it 'checks on both model and supermodel' do + expect(Pen.exists?(name: 'red pen')).to be_truthy + expect(Pen.exists?(name: 'red pen', price: 0.8)).to be_truthy + end + end + describe '.where and .where!' do it 'respects supermodel attributes' do conditions = { price: 0.8 } @@ -469,13 +510,13 @@ it 'works with hashes' do conditions = { - pen_caps: { size: 'M' }, - buyers: { name: 'Tim' } + pen_caps: { size: "M" }, + buyers: { name: "Tim" } } - expect(Pen.joins(:pen_caps, :buyers).where(conditions).to_a).to eq([@blue_pen]) + expect(Pen.joins(:pen_caps, product: :buyers).where(conditions).to_a).to eq([@blue_pen]) - relation = Pen.joins(:pen_caps, :buyers) + relation = Pen.joins(:pen_caps, product: :buyers) relation.where!(conditions) expect(relation.to_a).to eq([@blue_pen]) end @@ -540,7 +581,7 @@ Object.send(:remove_const, :Pen) end - it "should not include the selected attribute when associating using 'eager_load'" do + it "should include the selected attribute when associating using 'eager_load'" do class Pen < ActiveRecord::Base acts_as :product , {association_method: :eager_load} store_accessor :settings, :option1 @@ -548,7 +589,7 @@ class Pen < ActiveRecord::Base end Pen.create pen_attributes - expect(Pen.select("'something' as thing").first['thing']).to be_nil + expect(Pen.select("'something' as thing").first['thing']).to eq 'something' end it "should include the selected attribute in the model when associating using 'includes'" do diff --git a/spec/find_or_initialize_by_spec.rb b/spec/find_or_initialize_by_spec.rb new file mode 100644 index 0000000..2bdde6e --- /dev/null +++ b/spec/find_or_initialize_by_spec.rb @@ -0,0 +1,23 @@ +require 'models' + +RSpec.describe 'Model Initialization' do + subject { Pen } + + let(:pen_attributes) { { name: 'pen', color: 'red' } } + + before(:each) { clear_database } + + it 'find_or_initialize_by works' do + pen = subject.find_or_initialize_by(pen_attributes) + expect(pen.persisted?).to be false + expect(pen.name).to eq(pen_attributes[:name]) + expect(pen.color).to eq(pen_attributes[:color]) + end + + it 'where.first_or_initialize works' do + pen = subject.where(pen_attributes).first_or_initialize + expect(pen.persisted?).to be false + expect(pen.name).to eq(pen_attributes[:name]) + expect(pen.color).to eq(pen_attributes[:color]) + end +end diff --git a/spec/migrations_spec.rb b/spec/migrations_spec.rb index eda4e25..4339df0 100644 --- a/spec/migrations_spec.rb +++ b/spec/migrations_spec.rb @@ -1,4 +1,5 @@ require 'database_helper' +require 'models' require 'active_record/acts_as' class Product < ActiveRecord::Base diff --git a/spec/models.rb b/spec/models.rb index 1e7f70e..caaf041 100644 --- a/spec/models.rb +++ b/spec/models.rb @@ -16,6 +16,10 @@ def self.class_method 'class_method' end + callable_by_submodel def self.class_keyword_method_callable_by_submodel(one: 1, two: 2) + [one, two] + end + callable_by_submodel def self.class_method_callable_by_submodel 'class_method_callable_by_submodel' end @@ -27,6 +31,10 @@ def present def raise_error specific.non_existant_method end + + def keyword_method(one: 1, two: 2) + [one, two] + end end class Payment < ActiveRecord::Base @@ -50,6 +58,10 @@ def pen_instance_method end end +class Eraser < ActiveRecord::Base + acts_as :product +end + class Buyer < ActiveRecord::Base belongs_to :product end @@ -91,6 +103,15 @@ class PenLid < ActiveRecord::Base def initialize_schema initialize_database do + create_table :users do |t| + t.timestamps null: true + t.actable + end + + create_table :customers do |t| + t.integer :identifier + end + create_table :pen_collections do |t| t.timestamps null: true end @@ -101,6 +122,11 @@ def initialize_schema t.integer :pen_collection_id end + create_table :erasers do |t| + t.integer :strength + t.datetime :designed_at + end + create_table :products do |t| t.string :name t.float :price diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 82a9aba..a27edcd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,3 +11,9 @@ if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks=) ActiveRecord::Base.raise_in_transactional_callbacks = true end + +if ActiveRecord.version > Gem::Version.new('6.2') + ActiveRecord.use_yaml_unsafe_load = true +else + ActiveRecord::Base.use_yaml_unsafe_load = true +end